'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 * * * 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 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; } }