summaryrefslogtreecommitdiff
path: root/extensions/TimedMediaHandler/WebVideoTranscode
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TimedMediaHandler/WebVideoTranscode')
-rw-r--r--extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php1192
-rw-r--r--extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php965
2 files changed, 2157 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;
+ }
+}
diff --git a/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php b/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php
new file mode 100644
index 00000000..d437d9c7
--- /dev/null
+++ b/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php
@@ -0,0 +1,965 @@
+<?php
+/**
+ * Job for transcode jobs
+ *
+ * @file
+ * @ingroup JobQueue
+ */
+
+/**
+ * Job for web video transcode
+ *
+ * Support two modes
+ * 1) non-free media transcode ( delays the media file being inserted, adds note to talk page once ready)
+ * 2) derivatives for video ( makes new sources for the asset )
+ *
+ * @ingroup JobQueue
+ */
+
+class WebVideoTranscodeJob extends Job {
+ var $targetEncodeFile = null;
+ var $sourceFilePath = null;
+
+ /**
+ * @var File
+ */
+ var $file;
+
+ public function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'webVideoTranscode', $title, $params, $id );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * Local function to debug output ( jobs don't have access to the maintenance output class )
+ * @param $msg string
+ */
+ private function output( $msg ){
+ print $msg . "\n";
+ }
+
+ /**
+ * @return File
+ */
+ private function getFile() {
+ if( !$this->file ){
+ $this->file = wfLocalFile( $this->title );
+ }
+ return $this->file;
+ }
+
+ /**
+ * @return string
+ */
+ private function getTargetEncodePath(){
+ if( !$this->targetEncodeFile ){
+ $file = $this->getFile();
+ $transcodeKey = $this->params[ 'transcodeKey' ];
+ $this->targetEncodeFile = WebVideoTranscode::getTargetEncodeFile( $file, $transcodeKey );
+ $this->targetEncodeFile->bind( $this );
+ }
+ return $this->targetEncodeFile->getPath();
+ }
+ /**
+ * purge temporary encode target
+ */
+ private function purgeTargetEncodeFile () {
+ if ( $this->targetEncodeFile ) {
+ $this->targetEncodeFile->purge();
+ $this->targetEncodeFile = null;
+ }
+ }
+
+ /**
+ * @return string|bool
+ */
+ private function getSourceFilePath(){
+ if( !$this->sourceFilePath ){
+ $file = $this->getFile();
+ $this->source = $file->repo->getLocalReference( $file->getPath() );
+ if ( !$this->source ) {
+ $this->sourceFilePath = false;
+ } else {
+ $this->sourceFilePath = $this->source->getPath();
+ }
+ }
+ return $this->sourceFilePath;
+ }
+
+ /**
+ * Update the transcode table with failure time and error
+ * @param $transcodeKey string
+ * @param $error string
+ *
+ */
+ private function setTranscodeError( $transcodeKey, $error ){
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update(
+ 'transcode',
+ array(
+ 'transcode_time_error' => $dbw->timestamp(),
+ 'transcode_error' => $error
+ ),
+ array(
+ 'transcode_image_name' => $this->getFile()->getName(),
+ 'transcode_key' => $transcodeKey
+ ),
+ __METHOD__
+ );
+ $this->setLastError( $error );
+ }
+
+ /**
+ * Run the transcode request
+ * @return boolean success
+ */
+ public function run() {
+ global $wgVersion, $wgFFmpeg2theoraLocation;
+ // get a local pointer to the file
+ $file = $this->getFile();
+
+ // Validate the file exists:
+ if( !$file ){
+ $this->output( $this->title . ': File not found ' );
+ return false;
+ }
+
+ // Validate the transcode key param:
+ $transcodeKey = $this->params['transcodeKey'];
+ // Build the destination target
+ if( ! isset( WebVideoTranscode::$derivativeSettings[ $transcodeKey ] )){
+ $error = "Transcode key $transcodeKey not found, skipping";
+ $this->output( $error );
+ $this->setLastError( $error );
+ return false;
+ }
+
+ // Validate the source exists:
+ if( !$this->getSourceFilePath() || !is_file( $this->getSourceFilePath() ) ){
+ $status = $this->title . ': Source not found ' . $this->getSourceFilePath();
+ $this->output( $status );
+ $this->setTranscodeError( $transcodeKey, $status );
+ return false;
+ }
+
+ $options = WebVideoTranscode::$derivativeSettings[ $transcodeKey ];
+
+ if ( isset( $options[ 'novideo' ] ) ) {
+ $this->output( "Encoding to audio codec: " . $options['audioCodec'] );
+ } else {
+ $this->output( "Encoding to codec: " . $options['videoCodec'] );
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ // Check if we have "already started" the transcode ( possible error )
+ $dbStartTime = $dbw->selectField( 'transcode', 'transcode_time_startwork',
+ array(
+ 'transcode_image_name' => $this->getFile()->getName(),
+ 'transcode_key' => $transcodeKey
+ ),
+ __METHOD__
+ );
+ if( ! is_null( $dbStartTime ) ){
+ $error = 'Error, running transcode job, for job that has already started';
+ $this->output( $error );
+ return true;
+ }
+
+ // Update the transcode table letting it know we have "started work":
+ $jobStartTimeCache = $dbw->timestamp();
+ $dbw->update(
+ 'transcode',
+ array( 'transcode_time_startwork' => $jobStartTimeCache ),
+ array(
+ 'transcode_image_name' => $this->getFile()->getName(),
+ 'transcode_key' => $transcodeKey
+ ),
+ __METHOD__
+ );
+ // Avoid contention and "server has gone away" errors as
+ // the transcode will take a very long time in some cases
+ $dbw->commit( __METHOD__, 'flush' );
+
+ // Check the codec see which encode method to call;
+ if ( isset( $options[ 'novideo' ] ) ) {
+ $status = $this->ffmpegEncode( $options );
+ } elseif( $options['videoCodec'] == 'theora' && $wgFFmpeg2theoraLocation !== false ){
+ $status = $this->ffmpeg2TheoraEncode( $options );
+ } elseif( $options['videoCodec'] == 'vp8' || $options['videoCodec'] == 'vp9' || $options['videoCodec'] == 'h264' || ( $options['videoCodec'] == 'theora' && $wgFFmpeg2theoraLocation === false ) ){
+ // Check for twopass:
+ if( isset( $options['twopass'] ) ){
+ // ffmpeg requires manual two pass
+ $status = $this->ffmpegEncode( $options, 1 );
+ if( $status && !is_string($status) ){
+ $status = $this->ffmpegEncode( $options, 2 );
+ }
+ } else {
+ $status = $this->ffmpegEncode( $options );
+ }
+ } else {
+ wfDebug( 'Error unknown codec:' . $options['videoCodec'] );
+ $status = 'Error unknown target encode codec:' . $options['videoCodec'];
+ }
+
+ // Remove any log files all useful info should be in status and or we are done with 2 passs encoding
+ $this->removeFffmpgeLogFiles();
+
+ // Do a quick check to confirm the job was not restarted or removed while we were transcoding
+ // Confirm the in memory $jobStartTimeCache matches db start time
+ $dbStartTime = $dbw->selectField( 'transcode', 'transcode_time_startwork',
+ array(
+ 'transcode_image_name' => $this->getFile()->getName(),
+ 'transcode_key' => $transcodeKey
+ )
+ );
+
+ // Check for ( hopefully rare ) issue of or job restarted while transcode in progress
+ if( $dbw->timestamp( $jobStartTimeCache ) != $dbw->timestamp( $dbStartTime ) ){
+ $this->output('Possible Error, transcode task restarted, removed, or completed while transcode was in progress');
+ // if an error; just error out, we can't remove temp files or update states, because the new job may be doing stuff.
+ if( $status !== true ){
+ $this->setTranscodeError( $transcodeKey, $status );
+ return false;
+ }
+ // else just continue with db updates, and when the new job comes around it won't start because it will see
+ // that the job has already been started.
+ }
+
+ // If status is oky and target does not exist, reset status
+ if( $status === true && !is_file( $this->getTargetEncodePath() ) ) {
+ $status = 'Target does not exist: ' . $this->getTargetEncodePath();
+ }
+ // If status is ok and target is larger than 0 bytes
+ if( $status === true && filesize( $this->getTargetEncodePath() ) > 0 ){
+ $file = $this->getFile();
+ $storeOptions = null;
+ if ( version_compare( $wgVersion, '1.23c', '>' ) &&
+ strpos( $options['type'], '/ogg' ) !== false &&
+ $file->getLength()
+ ) {
+ // Ogg files need a duration header for firefox
+ $storeOptions['headers']['X-Content-Duration'] = floatval( $file->getLength() );
+ }
+
+ // Copy derivative from the FS into storage at $finalDerivativeFilePath
+ $result = $file->getRepo()->quickImport(
+ $this->getTargetEncodePath(), // temp file
+ WebVideoTranscode::getDerivativeFilePath( $file, $transcodeKey ), // storage
+ $storeOptions
+ );
+ if ( !$result->isOK() ) {
+ // no need to invalidate all pages with video. Because all pages remain valid ( no $transcodeKey derivative )
+ // just clear the file page ( so that the transcode table shows the error )
+ $this->title->invalidateCache();
+ $this->setTranscodeError( $transcodeKey, $result->getWikiText() );
+ $status = false;
+ } else {
+ $bitrate = round( intval( filesize( $this->getTargetEncodePath() ) / $file->getLength() ) * 8 );
+ //wfRestoreWarnings();
+ // Update the transcode table with success time:
+ $dbw->update(
+ 'transcode',
+ array(
+ 'transcode_error' => '',
+ 'transcode_time_success' => $dbw->timestamp(),
+ 'transcode_final_bitrate' => $bitrate
+ ),
+ array(
+ 'transcode_image_name' => $this->getFile()->getName(),
+ 'transcode_key' => $transcodeKey,
+ ),
+ __METHOD__
+ );
+ $dbw->commit( __METHOD__, 'flush' );
+ WebVideoTranscode::invalidatePagesWithFile( $this->title );
+ }
+ } else {
+ // Update the transcode table with failure time and error
+ $this->setTranscodeError( $transcodeKey, $status );
+ // no need to invalidate all pages with video. Because all pages remain valid ( no $transcodeKey derivative )
+ // just clear the file page ( so that the transcode table shows the error )
+ $this->title->invalidateCache();
+ }
+ //done with encoding target, clean up
+ $this->purgeTargetEncodeFile();
+
+ // Clear the webVideoTranscode cache ( so we don't keep out dated table cache around )
+ WebVideoTranscode::clearTranscodeCache( $this->title->getDBkey() );
+
+ $url = WebVideoTranscode::getTranscodedUrlForFile( $file, $transcodeKey );
+ SquidUpdate::purge( array( $url ) );
+
+ if ($status !== true) {
+ $this->setLastError( $status );
+ }
+ return $status === true;
+ }
+
+ function removeFffmpgeLogFiles(){
+ $path = $this->getTargetEncodePath();
+ $dir = dirname( $path );
+ if ( is_dir( $dir ) ) {
+ $dh = opendir( $dir );
+ if ( $dh ) {
+ while ( ($file = readdir($dh)) !== false ) {
+ $log_path = "$dir/$file";
+ $ext = strtolower( pathinfo( $log_path, PATHINFO_EXTENSION ) );
+ if( $ext == 'log' && substr( $log_path, 0 , strlen($path) ) == $path ){
+ wfSuppressWarnings();
+ unlink( $log_path );
+ wfRestoreWarnings();
+ }
+ }
+ closedir( $dh );
+ }
+ }
+ }
+
+ /**
+ * Utility helper for ffmpeg and ffmpeg2theora mapping
+ * @param $options array
+ * @param $pass int
+ * @return bool|string
+ */
+ function ffmpegEncode( $options, $pass=0 ){
+ global $wgFFmpegLocation, $wgTranscodeBackgroundMemoryLimit;
+
+ if( !is_file( $this->getSourceFilePath() ) ) {
+ return "source file is missing, " . $this->getSourceFilePath() . ". Encoding failed.";
+ }
+
+ // Set up the base command
+ $cmd = wfEscapeShellArg( $wgFFmpegLocation ) . ' -y -i ' . wfEscapeShellArg( $this->getSourceFilePath() );
+
+
+ if( isset( $options['vpre'] ) ){
+ $cmd.= ' -vpre ' . wfEscapeShellArg( $options['vpre'] );
+ }
+
+ if ( isset( $options['novideo'] ) ) {
+ $cmd.= " -vn ";
+ } elseif( $options['videoCodec'] == 'vp8' || $options['videoCodec'] == 'vp9' ){
+ $cmd.= $this->ffmpegAddWebmVideoOptions( $options, $pass );
+ } elseif( $options['videoCodec'] == 'h264'){
+ $cmd.= $this->ffmpegAddH264VideoOptions( $options, $pass );
+ } elseif( $options['videoCodec'] == 'theora'){
+ $cmd.= $this->ffmpegAddTheoraVideoOptions( $options, $pass );
+ }
+ // Add size options:
+ $cmd .= $this->ffmpegAddVideoSizeOptions( $options ) ;
+
+ // Check for start time
+ if( isset( $options['starttime'] ) ){
+ $cmd.= ' -ss ' . wfEscapeShellArg( $options['starttime'] );
+ } else {
+ $options['starttime'] = 0;
+ }
+ // Check for end time:
+ if( isset( $options['endtime'] ) ){
+ $cmd.= ' -t ' . intval( $options['endtime'] ) - intval($options['starttime'] ) ;
+ }
+
+ if ( $pass == 1 || isset( $options['noaudio'] ) ) {
+ $cmd.= ' -an';
+ } else {
+ $cmd.= $this->ffmpegAddAudioOptions( $options, $pass );
+ }
+
+ if ( $pass != 0 ) {
+ $cmd.=" -pass " .wfEscapeShellArg( $pass ) ;
+ $cmd.=" -passlogfile " . wfEscapeShellArg( $this->getTargetEncodePath() .'.log' );
+ }
+ // And the output target:
+ if ($pass == 1) {
+ $cmd.= ' /dev/null';
+ } else{
+ $cmd.= " " . $this->getTargetEncodePath();
+ }
+
+ $this->output( "Running cmd: \n\n" .$cmd . "\n" );
+
+ // Right before we output remove the old file
+ $retval = 0;
+ $shellOutput = $this->runShellExec( $cmd, $retval );
+
+ if( $retval != 0 ){
+ return $cmd .
+ "\n\nExitcode: $retval\nMemory: $wgTranscodeBackgroundMemoryLimit\n\n" .
+ $shellOutput;
+ }
+ return true;
+ }
+
+ /**
+ * Adds ffmpeg shell options for h264
+ *
+ * @param $options
+ * @param $pass
+ * @return string
+ */
+ function ffmpegAddH264VideoOptions( $options, $pass ){
+ global $wgFFmpegThreads;
+ // Set the codec:
+ $cmd= " -threads " . intval( $wgFFmpegThreads ) . " -vcodec libx264";
+ // Check for presets:
+ if( isset( $options['preset'] ) ){
+ // Add the two vpre types:
+ switch( $options['preset'] ){
+ case 'ipod320':
+ $cmd .= " -profile:v baseline -preset slow -coder 0 -bf 0 -weightb 1 -level 13 -maxrate 768k -bufsize 3M";
+ break;
+ case '720p':
+ case 'ipod640':
+ $cmd .= " -profile:v baseline -preset slow -coder 0 -bf 0 -refs 1 -weightb 1 -level 31 -maxrate 10M -bufsize 10M";
+ break;
+ default:
+ // in the default case just pass along the preset to ffmpeg
+ $cmd.= " -vpre " . wfEscapeShellArg( $options['preset'] );
+ break;
+ }
+ }
+ if( isset( $options['videoBitrate'] ) ){
+ $cmd.= " -b " . wfEscapeShellArg ( $options['videoBitrate'] );
+ }
+ // Output mp4
+ $cmd.=" -f mp4";
+ return $cmd;
+ }
+
+ function ffmpegAddVideoSizeOptions( $options ){
+ $cmd = '';
+ // Get a local pointer to the file object
+ $file = $this->getFile();
+
+ // Check for aspect ratio ( we don't do anything with this right now)
+ if ( isset( $options['aspect'] ) ) {
+ $aspectRatio = $options['aspect'];
+ } else {
+ $aspectRatio = $file->getWidth() . ':' . $file->getHeight();
+ }
+ if (isset( $options['maxSize'] )) {
+ // Get size transform ( if maxSize is > file, file size is used:
+
+ list( $width, $height ) = WebVideoTranscode::getMaxSizeTransform( $file, $options['maxSize'] );
+ $cmd.= ' -s ' . intval( $width ) . 'x' . intval( $height );
+ } elseif (
+ (isset( $options['width'] ) && $options['width'] > 0 )
+ &&
+ (isset( $options['height'] ) && $options['height'] > 0 )
+ ){
+ $cmd.= ' -s ' . intval( $options['width'] ) . 'x' . intval( $options['height'] );
+ }
+
+ // Handle crop:
+ $optionMap = array(
+ 'cropTop' => '-croptop',
+ 'cropBottom' => '-cropbottom',
+ 'cropLeft' => '-cropleft',
+ 'cropRight' => '-cropright'
+ );
+ foreach( $optionMap as $name => $cmdArg ){
+ if( isset($options[$name]) ){
+ $cmd.= " $cmdArg " . wfEscapeShellArg( $options[$name] );
+ }
+ }
+ return $cmd;
+ }
+ /**
+ * Adds ffmpeg shell options for webm
+ *
+ * @param $options
+ * @param $pass
+ * @return string
+ */
+ function ffmpegAddWebmVideoOptions( $options, $pass ){
+ global $wgFFmpegThreads;
+
+ // Get a local pointer to the file object
+ $file = $this->getFile();
+
+ $cmd =' -threads ' . intval( $wgFFmpegThreads );
+
+ // check for presets:
+ if( isset($options['preset']) ){
+ if ($options['preset'] == "360p") {
+ $cmd.= " -vpre libvpx-360p";
+ } elseif ( $options['preset'] == "720p" ) {
+ $cmd.= " -vpre libvpx-720p";
+ } elseif ( $options['preset'] == "1080p" ) {
+ $cmd.= " -vpre libvpx-1080p";
+ }
+ }
+
+ // Add the boiler plate vp8 ffmpeg command:
+ $cmd.=" -skip_threshold 0 -bufsize 6000k -rc_init_occupancy 4000";
+
+ // Check for video quality:
+ if ( isset( $options['videoQuality'] ) && $options['videoQuality'] >= 0 ) {
+ // Map 0-10 to 63-0, higher values worse quality
+ $quality = 63 - intval( intval( $options['videoQuality'] )/10 * 63 );
+ $cmd .= " -qmin " . wfEscapeShellArg( $quality );
+ $cmd .= " -qmax " . wfEscapeShellArg( $quality );
+ }
+
+ // Check for video bitrate:
+ if ( isset( $options['videoBitrate'] ) ) {
+ $cmd.= " -qmin 1 -qmax 51";
+ $cmd.= " -vb " . wfEscapeShellArg( $options['videoBitrate'] * 1000 );
+ }
+ // Set the codec:
+ if ( $options['videoCodec'] === 'vp9' ) {
+ $cmd.= " -vcodec libvpx-vp9";
+ if ( isset( $options['tileColumns'] ) ) {
+ $cmd.= ' -tile-columns ' . wfEscapeShellArg( $options['tileColumns'] );
+ }
+ } else {
+ $cmd.= " -vcodec libvpx";
+ }
+
+ // Check for keyframeInterval
+ if( isset( $options['keyframeInterval'] ) ){
+ $cmd.= ' -g ' . wfEscapeShellArg( $options['keyframeInterval'] );
+ $cmd.= ' -keyint_min ' . wfEscapeShellArg( $options['keyframeInterval'] );
+ }
+ if( isset( $options['deinterlace'] ) ){
+ $cmd.= ' -deinterlace';
+ }
+
+ // Output WebM
+ $cmd.=" -f webm";
+
+ return $cmd;
+ }
+
+ /**
+ * Adds ffmpeg/avconv shell options for ogg
+ *
+ * Used only when $wgFFmpeg2theoraLocation set to false.
+ * Warning: does not create Ogg skeleton metadata track.
+ *
+ * @param $options
+ * @param $pass
+ * @return string
+ */
+ function ffmpegAddTheoraVideoOptions( $options, $pass ){
+ global $wgFFmpegThreads;
+
+ // Get a local pointer to the file object
+ $file = $this->getFile();
+
+ $cmd =' -threads ' . intval( $wgFFmpegThreads );
+
+ // Check for video quality:
+ if ( isset( $options['videoQuality'] ) && $options['videoQuality'] >= 0 ) {
+ // Map 0-10 to 63-0, higher values worse quality
+ $quality = 63 - intval( intval( $options['videoQuality'] )/10 * 63 );
+ $cmd .= " -qmin " . wfEscapeShellArg( $quality );
+ $cmd .= " -qmax " . wfEscapeShellArg( $quality );
+ }
+
+ // Check for video bitrate:
+ if ( isset( $options['videoBitrate'] ) ) {
+ $cmd.= " -qmin 1 -qmax 51";
+ $cmd.= " -vb " . wfEscapeShellArg( $options['videoBitrate'] * 1000 );
+ }
+ // Set the codec:
+ $cmd.= " -vcodec theora";
+
+ // Check for keyframeInterval
+ if( isset( $options['keyframeInterval'] ) ){
+ $cmd.= ' -g ' . wfEscapeShellArg( $options['keyframeInterval'] );
+ $cmd.= ' -keyint_min ' . wfEscapeShellArg( $options['keyframeInterval'] );
+ }
+ if( isset( $options['deinterlace'] ) ){
+ $cmd.= ' -deinterlace';
+ }
+ if( isset( $options['framerate'] ) ) {
+ $cmd.= ' -r ' . wfEscapeShellArg( $options['framerate'] );
+ }
+
+ // Output Ogg
+ $cmd.=" -f ogg";
+
+ return $cmd;
+ }
+
+ /**
+ * @param $options array
+ * @param $pass
+ * @return string
+ */
+ function ffmpegAddAudioOptions( $options, $pass ){
+ $cmd ='';
+ if( isset( $options['audioQuality'] ) ){
+ $cmd.= " -aq " . wfEscapeShellArg( $options['audioQuality'] );
+ }
+ if( isset( $options['audioBitrate'] )){
+ $cmd.= ' -ab ' . intval( $options['audioBitrate'] ) * 1000;
+ }
+ if( isset( $options['samplerate'] ) ){
+ $cmd.= " -ar " . wfEscapeShellArg( $options['samplerate'] );
+ }
+ if( isset( $options['channels'] ) ){
+ $cmd.= " -ac " . wfEscapeShellArg( $options['channels'] );
+ }
+
+ if( isset( $options['audioCodec'] ) ){
+ $encoders = array(
+ 'vorbis' => 'libvorbis',
+ 'opus' => 'libopus',
+ 'mp3' => 'libmp3lame',
+ );
+ if ( isset( $encoders[ $options['audioCodec'] ] ) ) {
+ $codec = $encoders[ $options['audioCodec'] ];
+ } else {
+ $codec = $options['audioCodec'];
+ }
+ $cmd.= " -acodec " . wfEscapeShellArg( $codec );
+ if ( $codec === 'aac' ) {
+ // the aac encoder is currently "experimental" in libav 9? :P
+ $cmd .= ' -strict experimental';
+ }
+ } else {
+ // if no audio codec set use vorbis :
+ $cmd.= " -acodec libvorbis ";
+ }
+ return $cmd;
+ }
+
+ /**
+ * ffmpeg2Theora mapping is much simpler since it is the basis of the the firefogg API
+ * @param $options array
+ * @return bool|string
+ */
+ function ffmpeg2TheoraEncode( $options ){
+ global $wgFFmpeg2theoraLocation, $wgTranscodeBackgroundMemoryLimit;
+
+ if( !is_file( $this->getSourceFilePath() ) ) {
+ return "source file is missing, " . $this->getSourceFilePath() . ". Encoding failed.";
+ }
+
+ // Set up the base command
+ $cmd = wfEscapeShellArg( $wgFFmpeg2theoraLocation ) . ' ' . wfEscapeShellArg( $this->getSourceFilePath() );
+
+ $file = $this->getFile();
+
+ if( isset( $options['maxSize'] ) ){
+ list( $width, $height ) = WebVideoTranscode::getMaxSizeTransform( $file, $options['maxSize'] );
+ $options['width'] = $width;
+ $options['height'] = $height;
+ $options['aspect'] = $width . ':' . $height;
+ unset( $options['maxSize'] );
+ }
+
+ // Add in the encode settings
+ foreach( $options as $key => $val ){
+ if( isset( self::$foggMap[$key] ) ){
+ if( is_array( self::$foggMap[$key] ) ){
+ $cmd.= ' '. implode(' ', self::$foggMap[$key] );
+ } elseif ($val == 'true' || $val === true){
+ $cmd.= ' '. self::$foggMap[$key];
+ } elseif ($val == 'false' || $val === false){
+ //ignore "false" flags
+ } else {
+ //normal get/set value
+ $cmd.= ' '. self::$foggMap[$key] . ' ' . wfEscapeShellArg( $val );
+ }
+ }
+ }
+
+ // Add the output target:
+ $outputFile = $this->getTargetEncodePath();
+ $cmd.= ' -o ' . wfEscapeShellArg ( $outputFile );
+
+ $this->output( "Running cmd: \n\n" .$cmd . "\n" );
+
+ $retval = 0;
+ $shellOutput = $this->runShellExec( $cmd, $retval );
+
+ // ffmpeg2theora returns 0 status on some errors, so also check for file
+ if( $retval != 0 || !is_file( $outputFile ) || filesize( $outputFile ) === 0 ){
+ return $cmd .
+ "\n\nExitcode: $retval\nMemory: $wgTranscodeBackgroundMemoryLimit\n\n" .
+ $shellOutput;
+ }
+ return true;
+ }
+
+ /**
+ * Runs the shell exec command.
+ * if $wgEnableBackgroundTranscodeJobs is enabled will mannage a background transcode task
+ * else it just directly passes off to wfShellExec
+ *
+ * @param $cmd String Command to be run
+ * @param $retval String, refrence variable to return the exit code
+ * @return string
+ */
+ public function runShellExec( $cmd, &$retval ){
+ global $wgTranscodeBackgroundTimeLimit,
+ $wgTranscodeBackgroundMemoryLimit,
+ $wgTranscodeBackgroundSizeLimit,
+ $wgEnableNiceBackgroundTranscodeJobs;
+
+ // For profiling
+ $caller = wfGetCaller();
+
+ // Check if background tasks are enabled
+ if( $wgEnableNiceBackgroundTranscodeJobs === false ){
+ // Directly execute the shell command:
+ $limits = array(
+ "filesize" => $wgTranscodeBackgroundSizeLimit,
+ "memory" => $wgTranscodeBackgroundMemoryLimit,
+ "time" => $wgTranscodeBackgroundTimeLimit
+ );
+ return wfShellExec( $cmd . ' 2>&1', $retval , array(), $limits,
+ array( 'profileMethod' => $caller ) );
+ }
+
+ $encodingLog = $this->getTargetEncodePath() . '.stdout.log';
+ $retvalLog = $this->getTargetEncodePath() . '.retval.log';
+ // Check that we can actually write to these files
+ //( no point in running the encode if we can't write )
+ wfSuppressWarnings();
+ if( ! touch( $encodingLog) || ! touch( $retvalLog ) ){
+ wfRestoreWarnings();
+ $retval = 1;
+ return "Error could not write to target location";
+ }
+ wfRestoreWarnings();
+
+ // Fork out a process for running the transcode
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ $errorMsg = '$wgEnableNiceBackgroundTranscodeJobs enabled but failed pcntl_fork';
+ $retval = 1;
+ $this->output( $errorMsg);
+ return $errorMsg;
+ } elseif ( $pid == 0) {
+ // we are the child
+ $this->runChildCmd( $cmd, $retval, $encodingLog, $retvalLog, $caller );
+ // dont remove any temp files in the child process, this is done
+ // once the parent is finished
+ $this->targetEncodeFile->preserve();
+ if ( $this->source instanceof TempFSFile ) {
+ $this->source->preserve();
+ }
+ // exit with the same code as the transcode:
+ exit( $retval );
+ } else {
+ // we are the parent monitor and return status
+ return $this->monitorTranscode($pid, $retval, $encodingLog, $retvalLog);
+ }
+ }
+
+ /**
+ * @param $cmd
+ * @param $retval
+ * @param $encodingLog
+ * @param $retvalLog
+ * @param string $caller The calling method
+ */
+ public function runChildCmd( $cmd, &$retval, $encodingLog, $retvalLog, $caller ){
+ global $wgTranscodeBackgroundTimeLimit,
+ $wgTranscodeBackgroundMemoryLimit,
+ $wgTranscodeBackgroundSizeLimit;
+ // In theory we should use pcntl_exec but not sure how to get the stdout, ensure
+ // we don't max php memory with the same protections provided by wfShellExec.
+
+ // pcntl_exec requires a direct path to the exe and arguments as an array:
+ //$cmd = explode(' ', $cmd );
+ //$baseCmd = array_shift( $cmd );
+ //print "run:" . $baseCmd . " args: " . print_r( $cmd, true );
+ //$status = pcntl_exec($baseCmd , $cmd );
+
+ // Directly execute the shell command:
+ //global $wgTranscodeBackgroundPriority;
+ //$status = wfShellExec( 'nice -n ' . $wgTranscodeBackgroundPriority . ' '. $cmd . ' 2>&1', $retval );
+ $limits = array(
+ "filesize" => $wgTranscodeBackgroundSizeLimit,
+ "memory" => $wgTranscodeBackgroundMemoryLimit,
+ "time" => $wgTranscodeBackgroundTimeLimit
+ );
+ $status = wfShellExec( $cmd . ' 2>&1', $retval , array(), $limits,
+ array( 'profileMethod' => $caller ) );
+
+ // Output the status:
+ wfSuppressWarnings();
+ file_put_contents( $encodingLog, $status );
+ // Output the retVal to the $retvalLog
+ file_put_contents( $retvalLog, $retval );
+ wfRestoreWarnings();
+ }
+
+ /**
+ * @param $pid
+ * @param $retval
+ * @param $encodingLog
+ * @param $retvalLog
+ * @return string
+ */
+ public function monitorTranscode( $pid, &$retval, $encodingLog, $retvalLog ){
+ global $wgTranscodeBackgroundTimeLimit, $wgLang;
+ $errorMsg = '';
+ $loopCount = 0;
+ $oldFileSize = 0;
+ $startTime = time();
+ $fileIsNotGrowing = false;
+
+ $this->output( "Encoding with pid: $pid \npcntl_waitpid: " . pcntl_waitpid( $pid, $status, WNOHANG OR WUNTRACED) .
+ "\nisProcessRunning: " . self::isProcessRunningKillZombie( $pid ) . "\n" );
+
+ // Check that the child process is still running ( note this does not work well with pcntl_waitpid
+ // for some reason :(
+ while( self::isProcessRunningKillZombie( $pid ) ) {
+ //$this->output( "$pid is running" );
+
+ // Check that the target file is growing ( every 5 seconds )
+ if( $loopCount == 10 ){
+ // only run check if we are outputing to target file
+ // ( two pass encoding does not output to target on first pass )
+ clearstatcache();
+ $newFileSize = is_file( $this->getTargetEncodePath() ) ? filesize( $this->getTargetEncodePath() ) : 0;
+ // Don't start checking for file growth until we have an initial positive file size:
+ if( $newFileSize > 0 ){
+ $this->output( $wgLang->formatSize( $newFileSize ). ' Total size, encoding ' .
+ $wgLang->formatSize( ( $newFileSize - $oldFileSize ) / 5 ) . ' per second' );
+ if( $newFileSize == $oldFileSize ){
+ if( $fileIsNotGrowing ){
+ $errorMsg = "Target File is not increasing in size, kill process.";
+ $this->output( $errorMsg );
+ // file is not growing in size, kill proccess
+ $retval = 1;
+
+ //posix_kill( $pid, 9);
+ self::killProcess( $pid );
+ break;
+ }
+ // Wait an additional 5 seconds of the file not growing to confirm
+ // the transcode is frozen.
+ $fileIsNotGrowing = true;
+ } else {
+ $fileIsNotGrowing = false;
+ }
+ $oldFileSize = $newFileSize;
+ }
+ // reset the loop counter
+ $loopCount = 0;
+ }
+
+ // Check if we have global job run-time has been exceeded:
+ if ( $wgTranscodeBackgroundTimeLimit && time() - $startTime > $wgTranscodeBackgroundTimeLimit ){
+ $errorMsg = "Encoding exceeded max job run time ( "
+ . TimedMediaHandler::seconds2npt( $wgTranscodeBackgroundTimeLimit ) . " ), kill process.";
+ $this->output( $errorMsg );
+ // File is not growing in size, kill proccess
+ $retval = 1;
+ //posix_kill( $pid, 9);
+ self::killProcess( $pid );
+ break;
+ }
+
+ // Sleep for one second before repeating loop
+ $loopCount++;
+ sleep( 1 );
+ }
+
+ $returnPcntl = pcntl_wexitstatus( $status );
+ // check status
+ wfSuppressWarnings();
+ $returnCodeFile = file_get_contents( $retvalLog );
+ wfRestoreWarnings();
+ //$this->output( "TranscodeJob:: Child pcntl return:". $returnPcntl . ' Log file exit code:' . $returnCodeFile . "\n" );
+
+ // File based exit code seems more reliable than pcntl_wexitstatus
+ $retval = $returnCodeFile;
+
+ // return the encoding log contents ( will be inserted into error table if an error )
+ // ( will be ignored and removed if success )
+ if( $errorMsg!= '' ){
+ $errorMsg.="\n\n";
+ }
+ return $errorMsg . file_get_contents( $encodingLog );
+ }
+
+ /**
+ * check if proccess is running and not a zombie
+ * @param $pid int
+ * @return bool
+ */
+ public static function isProcessRunningKillZombie( $pid ){
+ exec( "ps $pid", $processState );
+ if( !isset( $processState[1] ) ){
+ return false;
+ }
+ if( strpos( $processState[1], '<defunct>' ) !== false ){
+ // posix kill does not seem to work
+ //posix_kill( $pid, 9);
+ self::killProcess( $pid );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Kill Application PID
+ *
+ * @param $pid int
+ * @return bool
+ */
+ public static function killProcess( $pid ){
+ exec( "kill -9 $pid" );
+ exec( "ps $pid", $processState );
+ if( isset( $processState[1] ) ){
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Mapping between firefogg api and ffmpeg2theora command line
+ *
+ * This lets us share a common api between firefogg and WebVideoTranscode
+ * also see: http://firefogg.org/dev/index.html
+ */
+ public static $foggMap = array(
+ // video
+ 'width' => "--width",
+ 'height' => "--height",
+ 'maxSize' => "--max_size",
+ 'noUpscaling' => "--no-upscaling",
+ 'videoQuality'=> "-v",
+ 'videoBitrate' => "-V",
+ 'twopass' => "--two-pass",
+ 'optimize' => "--optimize",
+ 'framerate' => "-F",
+ 'aspect' => "--aspect",
+ 'starttime' => "--starttime",
+ 'endtime' => "--endtime",
+ 'cropTop' => "--croptop",
+ 'cropBottom' => "--cropbottom",
+ 'cropLeft' => "--cropleft",
+ 'cropRight' => "--cropright",
+ 'keyframeInterval'=> "--keyint",
+ 'denoise' => array("--pp", "de"),
+ 'deinterlace' => "--deinterlace",
+ 'novideo' => array("--novideo", "--no-skeleton"),
+ 'bufDelay' => "--buf-delay",
+ // audio
+ 'audioQuality' => "-a",
+ 'audioBitrate' => "-A",
+ 'samplerate' => "-H",
+ 'channels' => "-c",
+ 'noaudio' => "--noaudio",
+ // metadata
+ 'artist' => "--artist",
+ 'title' => "--title",
+ 'date' => "--date",
+ 'location' => "--location",
+ 'organization' => "--organization",
+ 'copyright' => "--copyright",
+ 'license' => "--license",
+ 'contact' => "--contact"
+ );
+
+}