diff options
Diffstat (limited to 'extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php')
-rw-r--r-- | extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php | 1192 |
1 files changed, 1192 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php b/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php new file mode 100644 index 00000000..e4d02556 --- /dev/null +++ b/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php @@ -0,0 +1,1192 @@ +<?php +/** + * WebVideoTranscode provides: + * encode keys + * encode settings + * + * extends api to return all the streams + * extends video tag output to provide all the available sources + */ + +/** + * Main WebVideoTranscode Class hold some constants and config values + */ +class WebVideoTranscode { + /** + * Key constants for the derivatives, + * this key is appended to the derivative file name + * + * If you update the wgDerivativeSettings for one of these keys + * and want to re-generate the video you should also update the + * key constant. ( Or just run a maintenance script to delete all + * the assets for a given profile ) + * + * Msg keys for derivatives are set as follows: + * $messages['timedmedia-derivative-200_200kbs.ogv'] => 'Ogg 200'; + */ + + // Ogg Profiles + const ENC_OGV_160P = '160p.ogv'; + const ENC_OGV_240P = '240p.ogv'; + const ENC_OGV_360P = '360p.ogv'; + const ENC_OGV_480P = '480p.ogv'; + const ENC_OGV_720P = '720p.ogv'; + const ENC_OGV_1080P = '1080p.ogv'; + + // WebM VP8/Vorbis profiles: + const ENC_WEBM_160P = '160p.webm'; + const ENC_WEBM_360P = '360p.webm'; + const ENC_WEBM_480P = '480p.webm'; + const ENC_WEBM_720P = '720p.webm'; + const ENC_WEBM_1080P = '1080p.webm'; + const ENC_WEBM_2160P = '2160p.webm'; + + // WebM VP9/Opus profiles: + const ENC_VP9_360P = '360p.vp9.webm'; + const ENC_VP9_480P = '480p.vp9.webm'; + const ENC_VP9_720P = '720p.vp9.webm'; + const ENC_VP9_1080P = '1080p.vp9.webm'; + const ENC_VP9_2160P = '2160p.vp9.webm'; + + // mp4 profiles: + const ENC_H264_320P = '320p.mp4'; + const ENC_H264_480P = '480p.mp4'; + const ENC_H264_720P = '720p.mp4'; + const ENC_H264_1080P = '1080p.mp4'; + const ENC_H264_2160P = '2160p.mp4'; + + const ENC_OGG_VORBIS = 'ogg'; + const ENC_OGG_OPUS = 'opus'; + const ENC_MP3 = 'mp3'; + const ENC_AAC = 'm4a'; + + // Static cache of transcode state per instantiation + public static $transcodeState = array() ; + + /** + * Encoding parameters are set via firefogg encode api + * + * For clarity and compatibility with passing down + * client side encode settings at point of upload + * + * http://firefogg.org/dev/index.html + */ + public static $derivativeSettings = array( + WebVideoTranscode::ENC_OGV_160P => + array( + 'maxSize' => '288x160', + 'videoBitrate' => '160', + 'framerate' => '15', + 'audioQuality' => '-1', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + //'twopass' => 'true', // temporarily disabled for broken ffmpeg2theora + 'optimize' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'theora', + 'type' => 'video/ogg; codecs="theora, vorbis"', + ), + WebVideoTranscode::ENC_OGV_240P => + array( + 'maxSize' => '426x240', + 'videoBitrate' => '512', + 'audioQuality' => '0', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + //'twopass' => 'true', // temporarily disabled for broken ffmpeg2theora + 'optimize' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'theora', + 'type' => 'video/ogg; codecs="theora, vorbis"', + ), + WebVideoTranscode::ENC_OGV_360P => + array( + 'maxSize' => '640x360', + 'videoBitrate' => '1024', + 'audioQuality' => '1', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + //'twopass' => 'true', // temporarily disabled for broken ffmpeg2theora + 'optimize' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'theora', + 'type' => 'video/ogg; codecs="theora, vorbis"', + ), + WebVideoTranscode::ENC_OGV_480P => + array( + 'maxSize' => '854x480', + 'videoBitrate' => '2048', + 'audioQuality' => '2', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + //'twopass' => 'true', // temporarily disabled for broken ffmpeg2theora + 'optimize' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'theora', + 'type' => 'video/ogg; codecs="theora, vorbis"', + ), + + WebVideoTranscode::ENC_OGV_720P => + array( + 'maxSize' => '1280x720', + 'videoQuality' => 6, + 'audioQuality' => 3, + 'noUpscaling' => 'true', + //'twopass' => 'true', // temporarily disabled for broken ffmpeg2theora + 'optimize' => 'true', + 'keyframeInterval' => '128', + 'videoCodec' => 'theora', + 'type' => 'video/ogg; codecs="theora, vorbis"', + ), + + WebVideoTranscode::ENC_OGV_1080P => + array( + 'maxSize' => '1920x1080', + 'videoQuality' => 6, + 'audioQuality' => 3, + 'noUpscaling' => 'true', + //'twopass' => 'true', // temporarily disabled for broken ffmpeg2theora + 'optimize' => 'true', + 'keyframeInterval' => '128', + 'videoCodec' => 'theora', + 'type' => 'video/ogg; codecs="theora, vorbis"', + ), + + // WebM transcode: + WebVideoTranscode::ENC_WEBM_160P => + array( + 'maxSize' => '288x160', + 'videoBitrate' => '256', + 'audioQuality' => '-1', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp8', + 'type' => 'video/webm; codecs="vp8, vorbis"', + ), + WebVideoTranscode::ENC_WEBM_360P => + array( + 'maxSize' => '640x360', + 'videoBitrate' => '512', + 'audioQuality' => '1', + 'samplerate' => '44100', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp8', + 'type' => 'video/webm; codecs="vp8, vorbis"', + ), + WebVideoTranscode::ENC_WEBM_480P => + array( + 'maxSize' => '854x480', + 'videoBitrate' => '1024', + 'audioQuality' => '2', + 'samplerate' => '44100', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp8', + 'type' => 'video/webm; codecs="vp8, vorbis"', + ), + WebVideoTranscode::ENC_WEBM_720P => + array( + 'maxSize' => '1280x720', + 'videoQuality' => 7, + 'audioQuality' => 3, + 'noUpscaling' => 'true', + 'videoCodec' => 'vp8', + 'type' => 'video/webm; codecs="vp8, vorbis"', + ), + WebVideoTranscode::ENC_WEBM_1080P => + array( + 'maxSize' => '1920x1080', + 'videoQuality' => 7, + 'audioQuality' => 3, + 'noUpscaling' => 'true', + 'videoCodec' => 'vp8', + 'type' => 'video/webm; codecs="vp8, vorbis"', + ), + WebVideoTranscode::ENC_WEBM_2160P => + array( + 'maxSize' => '4096x2160', + 'videoQuality' => 7, + 'audioQuality' => 3, + 'noUpscaling' => 'true', + 'videoCodec' => 'vp8', + 'type' => 'video/webm; codecs="vp8, vorbis"', + ), + + // WebM VP9 transcode: + WebVideoTranscode::ENC_VP9_360P => + array( + 'maxSize' => '640x360', + 'videoBitrate' => '256', + 'samplerate' => '48000', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp9', + 'audioCodec' => 'opus', + 'type' => 'video/webm; codecs="vp9, opus"', + ), + WebVideoTranscode::ENC_VP9_480P => + array( + 'maxSize' => '854x480', + 'videoBitrate' => '512', + 'samplerate' => '48000', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp9', + 'audioCodec' => 'opus', + 'type' => 'video/webm; codecs="vp9, opus"', + ), + WebVideoTranscode::ENC_VP9_720P => + array( + 'maxSize' => '1280x720', + 'videoBitrate' => '1024', + 'samplerate' => '48000', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp9', + 'audioCodec' => 'opus', + 'tileColumns' => '2', + 'type' => 'video/webm; codecs="vp9, opus"', + ), + WebVideoTranscode::ENC_VP9_1080P => + array( + 'maxSize' => '1920x1080', + 'videoBitrate' => '2048', + 'samplerate' => '48000', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp9', + 'audioCodec' => 'opus', + 'tileColumns' => '4', + 'type' => 'video/webm; codecs="vp9, opus"', + ), + WebVideoTranscode::ENC_VP9_2160P => + array( + 'maxSize' => '4096x2160', + 'videoBitrate' => '8192', + 'samplerate' => '48000', + 'noUpscaling' => 'true', + 'twopass' => 'true', + 'keyframeInterval' => '128', + 'bufDelay' => '256', + 'videoCodec' => 'vp9', + 'audioCodec' => 'opus', + 'tileColumns' => '4', + 'type' => 'video/webm; codecs="vp9, opus"', + ), + + // Losly defined per PCF guide to mp4 profiles: + // https://develop.participatoryculture.org/index.php/ConversionMatrix + // and apple HLS profile guide: + // https://developer.apple.com/library/ios/#documentation/networkinginternet/conceptual/streamingmediaguide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-DontLinkElementID_24 + + WebVideoTranscode::ENC_H264_320P => + array( + 'maxSize' => '480x320', + 'videoCodec' => 'h264', + 'preset' => 'ipod320', + 'videoBitrate' => '400k', + 'audioCodec' => 'aac', + 'channels' => '2', + 'audioBitrate' => '40k', + 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', + ), + + WebVideoTranscode::ENC_H264_480P => + array( + 'maxSize' => '640x480', + 'videoCodec' => 'h264', + 'preset' => 'ipod640', + 'videoBitrate' => '1200k', + 'audioCodec' => 'aac', + 'channels' => '2', + 'audioBitrate' => '64k', + 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', + ), + + WebVideoTranscode::ENC_H264_720P => + array( + 'maxSize' => '1280x720', + 'videoCodec' => 'h264', + 'preset' => '720p', + 'videoBitrate' => '2500k', + 'audioCodec' => 'aac', + 'channels' => '2', + 'audioBitrate' => '128k', + 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', + ), + + WebVideoTranscode::ENC_H264_1080P => + array( + 'maxSize' => '1920x1080', + 'videoCodec' => 'h264', + 'videoBitrate' => '5000k', + 'audioCodec' => 'aac', + 'channels' => '2', + 'audioBitrate' => '128k', + 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', + ), + WebVideoTranscode::ENC_H264_2160P => + array( + 'maxSize' => '4096x2160', + 'videoCodec' => 'h264', + 'videoBitrate' => '16384k', + 'audioCodec' => 'aac', + 'channels' => '2', + 'audioBitrate' => '128k', + 'type' => 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', + ), + + //Audio profiles + WebVideoTranscode::ENC_OGG_VORBIS => + array( + 'audioCodec' => 'vorbis', + 'audioQuality' => '1', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + 'novideo' => 'true', + 'type' => 'audio/ogg; codecs="vorbis"', + ), + WebVideoTranscode::ENC_OGG_OPUS => + array( + 'audioCodec' => 'opus', + 'audioQuality' => '1', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + 'novideo' => 'true', + 'type' => 'audio/ogg; codecs="opus"', + ), + WebVideoTranscode::ENC_MP3 => + array( + 'audioCodec' => 'mp3', + 'audioQuality' => '1', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + 'novideo' => 'true', + 'type' => 'audio/mpeg', + ), + WebVideoTranscode::ENC_AAC => + array( + 'audioCodec' => 'aac', + 'audioQuality' => '1', + 'samplerate' => '44100', + 'channels' => '2', + 'noUpscaling' => 'true', + 'novideo' => 'true', + 'type' => 'audio/mp4; codecs="mp4a.40.5"', + ), + ); + + /** + * @param $file File + * @param $transcodeKey string + * @return string + */ + static public function getDerivativeFilePath( $file, $transcodeKey ) { + return $file->getTranscodedPath( self::getTranscodeFileBaseName( $file, $transcodeKey ) ); + } + + /** + * Get the name to use as the base name for the transcode. + * + * Swift has problems where the url-encoded version of + * the path (ie 'filename.ogv/filename.ogv.720p.webm' ) + * is greater that > 1024 bytes, so shorten in that case. + * + * Future versions might respect FileRepo::$abbrvThreshold. + * + * @param File $file + * @param String $suffix Optional suffix (e.g. transcode key). + * @return String File name, or the string transcode. + */ + static public function getTranscodeFileBaseName( $file, $suffix = '' ) { + $name = $file->getName(); + if ( strlen( urlencode( $name ) ) * 2 + 12 > 1024 ) { + return 'transcode' . '.' . $suffix; + } else { + return $name . '.' . $suffix; + } + } + + /** + * Get url for a transcode. + * + * @param $file File + * @param $suffix string Transcode key + * @return string + */ + static public function getTranscodedUrlForFile( $file, $suffix = '' ) { + return $file->getTranscodedUrl( self::getTranscodeFileBaseName( $file, $suffix ) ); + } + + /** + * Get temp file at target path for video encode + * + * @param $file File + * @param $transcodeKey String + * + * @return TempFSFile at target encode path + */ + static public function getTargetEncodeFile( &$file, $transcodeKey ){ + $filePath = self::getDerivativeFilePath( $file, $transcodeKey ); + $ext = strtolower( pathinfo( "$filePath", PATHINFO_EXTENSION ) ); + + // Create a temp FS file with the same extension + $tmpFile = TempFSFile::factory( 'transcode_' . $transcodeKey, $ext); + if ( !$tmpFile ) { + return False; + } + return $tmpFile; + } + + /** + * Get the max size of the web stream ( constant bitrate ) + * @return int + */ + static public function getMaxSizeWebStream(){ + global $wgEnabledTranscodeSet; + $maxSize = 0; + foreach( $wgEnabledTranscodeSet as $transcodeKey ){ + if( isset( self::$derivativeSettings[$transcodeKey]['videoBitrate'] ) ){ + $currentSize = self::$derivativeSettings[$transcodeKey]['maxSize']; + if( $currentSize > $maxSize ){ + $maxSize = $currentSize; + } + } + } + return $maxSize; + } + + /** + * Give a rough estimate on file size + * Note this is not always accurate.. especially with variable bitrate codecs ;) + * @param $file File + * @param $transcodeKey string + * @return number + */ + static public function getProjectedFileSize( $file, $transcodeKey ){ + $settings = self::$derivativeSettings[$transcodeKey]; + if( $settings[ 'videoBitrate' ] && $settings['audioBitrate'] ){ + return $file->getLength() * 8 * ( + self::$derivativeSettings[$transcodeKey]['videoBitrate'] + + + self::$derivativeSettings[$transcodeKey]['audioBitrate'] + ); + } + // Else just return the size of the source video ( we have no idea how large the actual derivative size will be ) + return $file->getLength() * $file->getHandler()->getBitrate( $file ) * 8; + } + + /** + * Static function to get the set of video assets + * Checks if the file is local or remote and grabs respective sources + * @param $file File + * @param $options array + * @return array|mixed + */ + static public function getSources( &$file , $options = array() ){ + if( $file->isLocal() || $file->repo instanceof ForeignDBViaLBRepo ){ + return self::getLocalSources( $file , $options ); + } else { + return self::getRemoteSources( $file , $options ); + } + } + + /** + * Grabs sources from the remote repo via ApiQueryVideoInfo.php entry point. + * + * TODO: This method could use some rethinking. See comments on PS1 of + * <https://gerrit.wikimedia.org/r/#/c/117916/> + * + * Because this works with commons regardless of whether TimedMediaHandler is installed or not + * @param $file File + * @param $options array + * @return array|mixed + */ + static public function getRemoteSources(&$file , $options = array() ){ + global $wgMemc; + // Setup source attribute options + $dataPrefix = in_array( 'nodata', $options )? '': 'data-'; + + // Use descriptionCacheExpiry as our expire for timed text tracks info + if ( $file->repo->descriptionCacheExpiry > 0 ) { + wfDebug("Attempting to get sources from cache..."); + $key = $file->repo->getLocalCacheKey( 'WebVideoSources', 'url', $file->getName() ); + $sources = $wgMemc->get($key); + if ( $sources ) { + wfDebug("Success found sources in local cache\n"); + return $sources; + } + wfDebug("source cache miss\n"); + } + + wfDebug("Get Video sources from remote api for " . $file->getName() . "\n"); + $query = array( + 'action' => 'query', + 'prop' => 'videoinfo', + 'viprop' => 'derivatives', + 'titles' => MWNamespace::getCanonicalName( NS_FILE ) .':'. $file->getTitle()->mTextform + ); + + $data = $file->repo->fetchImageQuery( $query ); + + if( isset( $data['warnings'] ) && isset( $data['warnings']['query'] ) + && $data['warnings']['query']['*'] == "Unrecognized value for parameter 'prop': videoinfo" ) + { + // Commons does not yet have TimedMediaHandler. + // Use the normal file repo system single source: + return array( self::getPrimarySourceAttributes( $file, array( $dataPrefix ) ) ); + } + $sources = array(); + // Generate the source list from the data response: + if( isset( $data['query'] ) && $data['query']['pages'] ){ + $vidResult = array_shift( $data['query']['pages'] ); + if( isset( $vidResult['videoinfo'] ) ) { + $derResult = array_shift( $vidResult['videoinfo'] ); + $derivatives = $derResult['derivatives']; + foreach( $derivatives as $derivativeSource ){ + $sources[] = $derivativeSource; + } + } + } + + // Update the cache: + if ( $sources && $file->repo->descriptionCacheExpiry > 0 ) { + $wgMemc->set( $key, $sources, $file->repo->descriptionCacheExpiry ); + } + + return $sources; + + } + + /** + * Based on the $wgEnabledTranscodeSet set of enabled derivatives we + * return sources that are ready. + * + * This will not automatically update or queue anything! + * + * @param $file File object + * @param $options array Options, a set of options: + * 'nodata' Strips the data- attribute, useful when your output is not html + * @return array an associative array of sources suitable for <source> tag output + */ + static public function getLocalSources( &$file , $options=array() ){ + global $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet, $wgEnableTranscode; + $sources = array(); + + // Add the original file: + $sources[] = self::getPrimarySourceAttributes( $file, $options ); + + // If $wgEnableTranscode is false don't look for or add other local sources: + if( $wgEnableTranscode === false && + !($file->repo instanceof ForeignDBViaLBRepo) ){ + return $sources; + } + + // If an "oldFile" don't look for other sources: + if( $file->isOld() ){ + return $sources; + } + + // Now Check for derivatives + if( $file->getHandler()->isAudio( $file ) ){ + $transcodeSet = $wgEnabledAudioTranscodeSet; + } else { + $transcodeSet = $wgEnabledTranscodeSet; + } + foreach( $transcodeSet as $transcodeKey ){ + if ( self::isTranscodeEnabled( $file, $transcodeKey ) ) { + // Try and add the source + self::addSourceIfReady( $file, $sources, $transcodeKey, $options ); + } + } + + return $sources; + } + + /** + * Get the transcode state for a given filename and transcodeKey + * + * @param $fileName string + * @param $transcodeKey string + * @return bool + */ + public static function isTranscodeReady( $file, $transcodeKey ){ + + // Check if we need to populate the transcodeState cache: + $transcodeState = self::getTranscodeState( $file ); + + // If no state is found the cache for this file is false: + if( !isset( $transcodeState[ $transcodeKey ] ) ) { + return false; + } + // Else return boolean ready state ( if not null, then ready ): + return !is_null( $transcodeState[ $transcodeKey ]['time_success'] ); + } + + /** + * Clear the transcode state cache: + * @param String $fileName Optional fileName to clear transcode cache for + */ + public static function clearTranscodeCache( $fileName = null){ + if( $fileName ){ + unset( self::$transcodeState[ $fileName ] ); + } else { + self::$transcodeState = array(); + } + } + + /** + * Populates the transcode table with the current DB state of transcodes + * if transcodes are not found in the database their state is set to "false" + * + * @param {Object} File object + */ + public static function getTranscodeState( $file, $db = false ){ + global $wgTranscodeBackgroundTimeLimit; + $fileName = $file->getName(); + if( ! isset( self::$transcodeState[$fileName] ) ){ + if ( $db === false ) { + $db = $file->repo->getSlaveDB(); + } + // initialize the transcode state array + self::$transcodeState[ $fileName ] = array(); + $res = $db->select( 'transcode', + '*', + array( 'transcode_image_name' => $fileName ), + __METHOD__, + array( 'LIMIT' => 100 ) + ); + $overTimeout = array(); + $over = $db->timestamp(time() - (2 * $wgTranscodeBackgroundTimeLimit)); + // Populate the per transcode state cache + foreach ( $res as $row ) { + // strip the out the "transcode_" from keys + $trascodeState = array(); + foreach( $row as $k => $v ){ + $trascodeState[ str_replace( 'transcode_', '', $k ) ] = $v; + } + self::$transcodeState[ $fileName ][ $row->transcode_key ] = $trascodeState; + if ( $row->transcode_time_startwork != NULL + && $row->transcode_time_startwork < $over + && $row->transcode_time_success == NULL + && $row->transcode_time_error == NULL ) { + $overTimeout[] = $row->transcode_key; + } + } + if ( $overTimeout ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( + 'transcode', + array( + 'transcode_time_error' => $dbw->timestamp(), + 'transcode_error' => 'timeout' + ), + array( + 'transcode_image_name' => $fileName, + 'transcode_key' => $overTimeout + ), + __METHOD__, + array( 'LIMIT' => count( $overTimeout ) ) + ); + } + } + $sorted = self::$transcodeState[ $fileName ]; + uksort( $sorted, 'strnatcmp' ); + return $sorted; + } + + /** + * Remove any transcode files and db states associated with a given $file + * Note that if you want to see them again, you must re-queue them by calling + * startJobQueue() or updateJobQueue(). + * + * also remove the transcode files: + * @param $file File Object + * @param $transcodeKey String Optional transcode key to remove only this key + */ + public static function removeTranscodes( &$file, $transcodeKey = false ){ + + // if transcode key is non-false, non-null: + if( $transcodeKey ){ + // only remove the requested $transcodeKey + $removeKeys = array( $transcodeKey ); + } else { + // Remove any existing files ( regardless of their state ) + $res = $file->repo->getMasterDB()->select( 'transcode', + array( 'transcode_key' ), + array( 'transcode_image_name' => $file->getName() ) + ); + $removeKeys = array(); + foreach( $res as $transcodeRow ){ + $removeKeys[] = $transcodeRow->transcode_key; + } + } + + // Remove files by key: + $urlsToPurge = array(); + foreach ( $removeKeys as $tKey ) { + $urlsToPurge[] = self::getTranscodedUrlForFile( $file, $tKey ); + $filePath = self::getDerivativeFilePath( $file, $tKey ); + if( $file->repo->fileExists( $filePath ) ){ + wfSuppressWarnings(); + $res = $file->repo->quickPurge( $filePath ); + wfRestoreWarnings(); + if( !$res ){ + wfDebug( "Could not delete file $filePath\n" ); + } + } + } + + SquidUpdate::purge( $urlsToPurge ); + + // Build the sql query: + $dbw = wfGetDB( DB_MASTER ); + $deleteWhere = array( 'transcode_image_name' => $file->getName() ); + // Check if we are removing a specific transcode key + if( $transcodeKey !== false ){ + $deleteWhere['transcode_key'] = $transcodeKey; + } + // Remove the db entries + $dbw->delete( 'transcode', $deleteWhere, __METHOD__ ); + + // Purge the cache for pages that include this video: + $titleObj = $file->getTitle(); + self::invalidatePagesWithFile( $titleObj ); + + // Remove from local WebVideoTranscode cache: + self::clearTranscodeCache( $titleObj->getDBkey() ); + } + + /** + * @param $titleObj Title + */ + public static function invalidatePagesWithFile( &$titleObj ){ + wfDebug("WebVideoTranscode:: Invalidate pages that include: " . $titleObj->getDBkey() . "\n" ); + // Purge the main image page: + $titleObj->invalidateCache(); + + // TODO if the video is used in over 500 pages add to 'job queue' + // TODO interwiki invalidation ? + $limit = 500; + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( + array( 'imagelinks', 'page' ), + array( 'page_namespace', 'page_title' ), + array( 'il_to' => $titleObj->getDBkey(), 'il_from = page_id' ), + __METHOD__, + array( 'LIMIT' => $limit + 1 ) + ); + foreach ( $res as $page ) { + $title = Title::makeTitle( $page->page_namespace, $page->page_title ); + $title->invalidateCache(); + } + } + + /** + * Add a source to the sources list if the transcode job is ready + * + * If the source is not found, it will not be used yet... + * Missing transcodes should be added by write tasks, not read tasks! + */ + public static function addSourceIfReady( &$file, &$sources, $transcodeKey, $dataPrefix = '' ){ + // Check if the transcode is ready: + if( self::isTranscodeReady( $file, $transcodeKey ) ){ + $sources[] = self::getDerivativeSourceAttributes( $file, $transcodeKey, $dataPrefix ); + } + } + + /** + * Get the primary "source" asset used for other derivatives + * @param $file File + * @param $options array + * @return array + */ + static public function getPrimarySourceAttributes( $file, $options = array() ){ + global $wgLang; + $src = in_array( 'fullurl', $options)? wfExpandUrl( $file->getUrl() ) : $file->getUrl(); + + $bitrate = $file->getHandler()->getBitrate( $file ); + $metadataType = $file->getHandler()->getMetadataType( $file ); + + // Give grep a chance to find the usages: timedmedia-ogg, timedmedia-webm, + // timedmedia-mp4, timedmedia-flac, timedmedia-wav + if( $file->getHandler()->isAudio( $file ) ){ + $title = wfMessage( 'timedmedia-source-audio-file-desc', + wfMessage( 'timedmedia-' . $metadataType )->text() ) + ->params( $wgLang->formatBitrate( $bitrate ) )->text(); + } else { + $title = wfMessage( 'timedmedia-source-file-desc', + wfMessage( 'timedmedia-' . $metadataType )->text() ) + ->numParams( $file->getWidth(), $file->getHeight() ) + ->params( $wgLang->formatBitrate( $bitrate ) )->text(); + } + + // Give grep a chance to find the usages: timedmedia-ogg, timedmedia-webm, + // timedmedia-mp4, timedmedia-flac, timedmedia-wav + $source = array( + 'src' => $src, + 'type' => $file->getHandler()->getWebType( $file ), + 'title' => $title, + "shorttitle" => wfMessage( + 'timedmedia-source-file', + wfMessage( 'timedmedia-' . $metadataType )->text() + )->text(), + "width" => intval( $file->getWidth() ), + "height" => intval( $file->getHeight() ), + ); + + if( $bitrate ){ + $source["bandwidth"] = round ( $bitrate ); + } + + // For video include framerate: + if( !$file->getHandler()->isAudio( $file ) ){ + $framerate = $file->getHandler()->getFramerate( $file ); + if( $framerate ){ + $source[ "framerate" ] = floatval( $framerate ); + } + } + return $source; + } + + /** + * Get derivative "source" attributes + * @param $file File + * @param $transcodeKey string + * @param $options array + * @return array + */ + static public function getDerivativeSourceAttributes($file, $transcodeKey, $options = array() ){ + $fileName = $file->getTitle()->getDbKey(); + + $src = self::getTranscodedUrlForFile( $file, $transcodeKey ); + + if( $file->getHandler()->isAudio( $file ) ){ + $width = $height = 0; + } else { + list( $width, $height ) = WebVideoTranscode::getMaxSizeTransform( + $file, + self::$derivativeSettings[$transcodeKey]['maxSize'] + ); + } + + $framerate = ( isset( self::$derivativeSettings[$transcodeKey]['framerate'] ) )? + self::$derivativeSettings[$transcodeKey]['framerate'] : + $file->getHandler()->getFramerate( $file ); + // Setup the url src: + $src = in_array( 'fullurl', $options) ? wfExpandUrl( $src ) : $src; + $fields = array( + 'src' => $src, + 'title' => wfMessage( 'timedmedia-derivative-desc-' . $transcodeKey )->text(), + 'type' => self::$derivativeSettings[ $transcodeKey ][ 'type' ], + "shorttitle" => wfMessage( 'timedmedia-derivative-' . $transcodeKey )->text(), + "transcodekey" => $transcodeKey, + + // Add data attributes per emerging DASH / webTV adaptive streaming attributes + // eventually we will define a manifest xml entry point. + "width" => intval( $width ), + "height" => intval( $height ), + ); + + // a "ready" transcode should have a bitrate: + if ( isset( self::$transcodeState[$fileName] ) ) { + $fields["bandwidth"] = intval( + self::$transcodeState[$fileName][ $transcodeKey ]['final_bitrate'] + ); + } + + if ( !$file->getHandler()->isAudio( $file ) ) { + $fields += array( "framerate" => floatval( $framerate ) ); + } + return $fields; + } + + /** + * Queue up all enabled transcodes if missing. + * @param $file File object + */ + public static function startJobQueue( File $file ) { + global $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet; + $keys = array_merge( $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet ); + + // 'Natural sort' puts the transcodes in ascending order by resolution, + // which roughly gives us fastest-to-slowest order. + natsort($keys); + + foreach ( $keys as $tKey ) { + // Note the job queue will de-duplicate and handle various errors, so we + // can just blast out the full list here. + self::updateJobQueue( $file, $tKey ); + } + } + + /** + * Make sure all relevant transcodes for the given file are tracked in the + * transcodes table; add entries for any missing ones. + * + * @param $file File object + */ + public static function cleanupTranscodes( File $file ) { + global $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet; + + $fileName = $file->getTitle()->getDbKey(); + $db = $file->repo->getMasterDB(); + + $transcodeState = self::getTranscodeState( $file, $db ); + + $keys = array_merge( $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet ); + foreach ( $keys as $transcodeKey ) { + if ( !self::isTranscodeEnabled( $file, $transcodeKey ) ) { + // This transcode is no longer enabled or erroneously included... + // Leave it in place, allowing it to be removed manually; + // it won't be used in playback and should be doing no harm. + continue; + } + if ( !isset( $transcodeState[ $transcodeKey ] ) ) { + $db->insert( + 'transcode', + array( + 'transcode_image_name' => $fileName, + 'transcode_key' => $transcodeKey, + 'transcode_time_addjob' => null, + 'transcode_error' => "", + 'transcode_final_bitrate' => 0 + ), + __METHOD__, + array( 'IGNORE' ) + ); + } + } + } + + /** + * Check if the given transcode key is appropriate for the file. + * + * @param $file File object + * @param $transcodeKey String transcode key + * @return boolean + */ + public static function isTranscodeEnabled( File $file, $transcodeKey ) { + global $wgEnabledTranscodeSet, $wgEnabledAudioTranscodeSet; + + $audio = $file->getHandler()->isAudio( $file ); + if ( $audio ) { + $keys = $wgEnabledAudioTranscodeSet; + } else { + $keys = $wgEnabledTranscodeSet; + } + + if ( in_array( $transcodeKey, $keys ) ) { + $settings = self::$derivativeSettings[$transcodeKey]; + if ( $audio ) { + $sourceCodecs = $file->getHandler()->getStreamTypes( $file ); + $sourceCodec = $sourceCodecs ? strtolower( $sourceCodecs[0] ) : ''; + return ( $sourceCodec !== $settings['audioCodec'] ); + } else if ( self::isTargetLargerThanFile( $file, $settings['maxSize'] ) ) { + // Are we the smallest enabled transcode for this type? + // Then go ahead and make a wee little transcode for compat. + return self::isSmallestTranscodeForCodec( $transcodeKey ); + } else { + return true; + } + } else { + // Transcode key is invalid or has been disabled. + return false; + } + } + + /** + * Update the job queue if the file is not already in the job queue: + * @param $file File object + * @param $transcodeKey String transcode key + */ + public static function updateJobQueue( &$file, $transcodeKey ){ + $fileName = $file->getTitle()->getDbKey(); + $db = $file->repo->getMasterDB(); + + $transcodeState = self::getTranscodeState( $file, $db ); + + if ( !self::isTranscodeEnabled( $file, $transcodeKey ) ) { + return; + } + + // If the job hasn't been added yet, attempt to do so + if ( !isset( $transcodeState[ $transcodeKey ] ) ) { + $db->insert( + 'transcode', + array( + 'transcode_image_name' => $fileName, + 'transcode_key' => $transcodeKey, + 'transcode_time_addjob' => $db->timestamp(), + 'transcode_error' => "", + 'transcode_final_bitrate' => 0 + ), + __METHOD__, + array( 'IGNORE' ) + ); + + if ( !$db->affectedRows() ) { + // There is already a row for that job added by another request, no need to continue + return; + } + + $job = new WebVideoTranscodeJob( $file->getTitle(), array( + 'transcodeMode' => 'derivative', + 'transcodeKey' => $transcodeKey, + ) ); + + if ( $job->insert() ) { + // Clear the state cache ( now that we have updated the page ) + self::clearTranscodeCache( $fileName ); + } else { + // Adding job failed, update transcode row + $db->update( + 'transcode', + array( + 'transcode_time_error' => $db->timestamp(), + 'transcode_error' => "Failed to insert Job." + ), + array( + 'transcode_image_name' => $fileName, + 'transcode_key' => $transcodeKey, + ), + __METHOD__, + array( 'LIMIT' => 1 ) + ); + } + } + } + + /** + * Transforms the size per a given "maxSize" + * if maxSize is > file, file size is used + * @param $file File + * @param $targetMaxSize int + * @return array + */ + public static function getMaxSizeTransform( &$file, $targetMaxSize ){ + $maxSize = self::getMaxSize( $targetMaxSize ); + $sourceWidth = intval( $file->getWidth() ); + $sourceHeight = intval( $file->getHeight() ); + if ( $sourceHeight === 0 ) { + // Audio file + return array( 0, 0 ); + } + $sourceAspect = $sourceWidth / $sourceHeight; + $targetWidth = $sourceWidth; + $targetHeight = $sourceHeight; + if ( $sourceAspect <= $maxSize['aspect'] ) { + if ( $sourceHeight > $maxSize['height'] ) { + $targetHeight = $maxSize['height']; + $targetWidth = intval( $targetHeight * $sourceAspect ); + } + } else { + if ( $sourceWidth > $maxSize['width'] ) { + $targetWidth = $maxSize['width']; + $targetHeight = intval( $targetWidth / $sourceAspect ); + //some players do not like uneven frame sizes + } + } + //some players do not like uneven frame sizes + $targetWidth += $targetWidth%2; + $targetHeight += $targetHeight%2; + return array( $targetWidth, $targetHeight ); + } + + /** + * Test if a given transcode target is larger than the source file + * + * @param $file File object + * @param $targetMaxSize string + * @return bool + */ + public static function isTargetLargerThanFile( &$file, $targetMaxSize ){ + $maxSize = self::getMaxSize( $targetMaxSize ); + $sourceWidth = $file->getWidth(); + $sourceHeight = $file->getHeight(); + $sourceAspect = intval( $sourceWidth ) / intval( $sourceHeight ); + if ( $sourceAspect <= $maxSize['aspect'] ) { + return ( $maxSize['height'] > $sourceHeight ); + } else { + return ( $maxSize['width'] > $sourceWidth ); + } + } + + /** + * Is the given transcode key the smallest configured transcode for + * its video codec? + */ + public static function isSmallestTranscodeForCodec( $transcodeKey ) { + global $wgEnabledTranscodeSet; + + $settings = self::$derivativeSettings[$transcodeKey]; + $vcodec = $settings['videoCodec']; + $maxSize = self::getMaxSize( $settings['maxSize'] ); + + foreach ( $wgEnabledTranscodeSet as $tKey ) { + $tsettings = self::$derivativeSettings[$tKey]; + if ( $tsettings['videoCodec'] === $vcodec ) { + $tmaxSize = self::getMaxSize( $tsettings['maxSize'] ); + if ( $tmaxSize['width'] < $maxSize['width'] ) { + return false; + } + if ( $tmaxSize['height'] < $maxSize['height'] ) { + return false; + } + } + } + + return true; + } + + /** + * Return maxSize array for given maxSize setting + * + * @param $targetMaxSize string + * @return array + */ + public static function getMaxSize( $targetMaxSize ){ + $maxSize = array(); + $targetMaxSize = explode( 'x', $targetMaxSize ); + $maxSize['width'] = intval( $targetMaxSize[0] ); + if ( count( $targetMaxSize ) == 1 ) { + $maxSize['height'] = intval( $targetMaxSize[0] ); + } else { + $maxSize['height'] = intval( $targetMaxSize[1] ); + } + // check for zero size ( audio ) + if( $maxSize['width'] === 0 || $maxSize['height'] == 0 ){ + return 0; + } + $maxSize['aspect'] = $maxSize['width'] / $maxSize['height']; + return $maxSize; + } +} |