diff --git a/README.md b/README.md index f32f732..04dcf6d 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,24 @@ then `good-morning.mp4` will play every day, even the 15th. It takes priority be will cause Channel Two to play `good-morning.mp4` the first day at 7am, then `bad-morning.mp4` the second day, then `good-morning.mp4` the third day, etc. +## Closed Captions + +If you have WebVTT caption files for your videos, you can force them to be used as closed captions by adding the `captions=true` flag to a scheduling block: + +``` +captions=true +0 9 * * * /path/to/play-me-with-captions.mp4 + +captions=false +0 11 * * * /path/to/no-captions-please.mp4 +``` + +Channel Two will looks for a caption file in the same directory that is either the full filename plus ".vtt", or the filename with ".vtt" replacing the original file extension. For the example above, it would look for either `/path/to/play-me-with-captions.mp4.vtt` or `/path/to/play-me-with-captions.vtt`. + +While a program is playing, you can press the "c" to force captions to be turned on if they are not already enabled. + +Note that because of browser security policies, in order to use caption files, you must be running Channel Two through a Web server as described in the "Caveats" section below. + ## Generating the JavaScript programming file Any time you modify `schedule.txt`, you'll need to re-generate the JavaScript schedule file. Do this: diff --git a/channel-two.js b/channel-two.js index aa65515..3514cae 100644 --- a/channel-two.js +++ b/channel-two.js @@ -55,6 +55,12 @@ jQuery( function ( $ ) { var chyronTimerId = null; + /** + * Should captions be turned on regardless of the setting in the schedule? Press C to enable. + * Note that there's no way to turn captions off if they've been enabled in the schedule file. + */ + var captionOverride = false; + /** * Webpages can't autoplay video or go full-screen without some interaction from the user, * so require a button to be clicked to kick things off. @@ -472,9 +478,8 @@ jQuery( function ( $ ) { saveLastPlay( cron, nextFileToPlay ); - play( nextFileToPlay ); - tv.data( 'cron', cron ); + play( nextFileToPlay ); // If this cron pattern matches every minute, this content should end if something else should be run. if ( cron.indexOf( '* ' ) === 0 ) { @@ -521,9 +526,8 @@ jQuery( function ( $ ) { if ( nextFileToPlay ) { saveLastPlay( cron, nextFileToPlay ); - play( nextFileToPlay ); - tv.data( 'cron', cron ); + play( nextFileToPlay ); // If this cron pattern matches every minute, this content should end if something else should be run. if ( cron.indexOf( '* ' ) === 0 ) { @@ -563,7 +567,11 @@ jQuery( function ( $ ) { $( '#right-back' ).hide(); tv.removeAttr( 'src' ); + tv.empty(); + tv.attr( 'src', path ); + addCaptions( path ); + setCaptionStatus(); tvElement.load(); @@ -612,6 +620,49 @@ jQuery( function ( $ ) { } ); } + function addCaptions( path ) { + if ( logLevel >= 1 ) console.log( "addCaptions(" + path + ")" ); + + tv.empty(); + + let captions = getCaptions( path ); + + if ( captions ) { + let track = $( '' ) + .attr( 'kind', 'captions' ) + .attr( 'src', captions ) + .attr( 'default', 'default' ); + + tv.append( track ); + + if ( logLevel >= 1 ) console.log( "added captions" ); + } else { + if ( logLevel >= 1 ) console.log( "There were no captions to add." ); + } + } + + function setCaptionStatus() { + let currentCron = tv.data( 'cron' ); + + if ( 'textTracks' in tvElement ) { + if ( logLevel >= 2 ) console.log( "There are " + tvElement.textTracks.length + " text tracks." ); + + for ( let i = 0; i < tvElement.textTracks.length; i++ ) { + if ( captionOverride || ( currentCron in programming.schedule && programming.schedule[ currentCron ].flags.captions ) ) { + if ( logLevel >= 2 ) console.log( "Enabling text track " + i ); + + tvElement.textTracks[i].mode = 'showing'; + } else { + if ( logLevel >= 2 ) console.log( "Disabling text track " + i ); + + tvElement.textTracks[i].mode = 'hidden'; + } + } + } else { + if ( logLevel >= 1 ) console.log( "This browser does not support textTracks." ); + } + } + /** * Given a cron pattern, find the next video that should be played. * For a file or URL, it's the file or URL. For a directory, it's the video that comes alphabetically, or a random one if shuffle is enabled. @@ -655,7 +706,7 @@ jQuery( function ( $ ) { } } - console.log( "Potential files for " + cron + ": ", potentialFiles ); + if ( logLevel >= 2 ) console.log( "Potential files for " + cron + ": ", potentialFiles ); if ( programming.schedule[cron].flags.shuffle ) { if ( logLevel >= 2 ) console.log( "shuffle is enabled" ); @@ -751,7 +802,7 @@ jQuery( function ( $ ) { } } - console.log( "Potential files for " + cron + ": ", potentialFiles ); + if ( logLevel >= 2 ) console.log( "Potential files for " + cron + ": ", potentialFiles ); if ( programming.schedule[cron].flags.shuffle ) { if ( logLevel >= 2 ) console.log( "shuffle is enabled" ); @@ -893,6 +944,27 @@ jQuery( function ( $ ) { return false; } + /** + * If a caption file is available for a file, return its path. + * + * @return int|bool The duration, or false if not available. + */ + function getCaptions( filePath ) { + if ( logLevel >= 2 ) console.log( "getCaptions(" + filePath + ")" ); + + if ( filePath in programming.content_index ) { + if ( 'captions' in programming.content_index[ filePath ] ) { + return programming.content_index[ filePath ].captions; + } + } else if ( filePath in programming.ad_index ) { + if ( 'captions' in programming.ad_index[ filePath ] ) { + return programming.ad_index[ filePath ].captions; + } + } + + return false; + } + /** * Save the duration of a file that didn't have a stored duration in programming.js * @@ -1078,33 +1150,42 @@ jQuery( function ( $ ) { } let currentCron = tv.data( 'cron' ); - let secondsLeft = secondsLeftInCurrentProgram(); switch ( e.which ) { case 37: - // Left arrow - // Go to the previous episode that should have played, resuming with the same amount of time left in the current episode. - let previousContent = getPreviousContentFromCron( currentCron ); + case 39: + // Left or right arrow + let secondsLeft = secondsLeftInCurrentProgram(); - programmingQueue.push( { - src: previousContent + '#t=' + ( -1 * secondsLeft ), - cron : currentCron - } ); + let switchToThisContent; - queueNextProgramming(); - break; - case 39: - // Right arrow - // Go to the next episode that should play, resuming with the same amount of time left in the current episode. - let nextContent = getNextContentFromCron( currentCron ); + if ( e.which == 37 ) { + // Go to the previous episode that should have played, resuming with the same amount of time left in the current episode. + switchToThisContent = getPreviousContentFromCron( currentCron ); + } else { + // 39: right arrow + // Go to the next episode that should play, resuming with the same amount of time left in the current episode. + + switchToThisContent = getNextContentFromCron( currentCron ); + } programmingQueue.push( { - src: nextContent + '#t=' + ( -1 * secondsLeft ), + src: switchToThisContent + '#t=' + ( -1 * secondsLeft ), cron : currentCron } ); queueNextProgramming(); break; + case 67: + // c for captions + if ( logLevel >= 2 ) console.log( "c keyup" ); + + captionOverride = ! captionOverride; + setCaptionStatus(); + break; + default: + if ( logLevel >= 2 ) console.log( e.which ); + break; } } ); } ); \ No newline at end of file diff --git a/parse-schedule.php b/parse-schedule.php index 2df73d5..732f469 100644 --- a/parse-schedule.php +++ b/parse-schedule.php @@ -39,6 +39,7 @@ $flags = array( 'ads' => false, 'shuffle' => false, + 'captions' => false, ); $existing_programming = false; @@ -95,7 +96,7 @@ function existing_duration( $path ) { } - $valid_flags = array( 'ads', 'shuffle' ); + $valid_flags = array( 'ads', 'shuffle', 'captions', ); if ( preg_match( '/^(' . join( '|', $valid_flags ) . ')=/', $line ) ) { // Variable. @@ -117,10 +118,11 @@ function existing_duration( $path ) { } break; case 'shuffle': + case 'captions': if ( strtolower( $parts[1] ) == 'true' ) { - $flags['shuffle'] = true; + $flags[ $parts[0] ] = true; } else { - $flags['shuffle'] = false; + $flags[ $parts[0] ] = false; } break; } @@ -232,6 +234,18 @@ function index_file( $file_path, &$index ) { $content_data->duration = $duration; } + $possible_caption_files = array( + $file_path . ".vtt", + preg_replace( '/\.[^\.]+$/', '.vtt', $file_path ), + ); + + foreach ( $possible_caption_files as $possible_caption_file ) { + if ( file_exists( $possible_caption_file ) ) { + $content_data->captions = substr( $possible_caption_file, max( 0, strlen( $base ) - 1 ) ); + break; + } + } + $index[ substr( $file_path, max( 0, strlen( $base ) - 1 ) ) ] = $content_data; } }