| Current File : /home/jvzmxxx/wiki1/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php |
<?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 TempFSFile */
public $targetEncodeFile = null;
/** @var string */
public $sourceFilePath = null;
/** @var File */
public $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',
[
'transcode_time_error' => $dbw->timestamp(),
'transcode_error' => $error
],
[
'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',
[
'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',
[ 'transcode_time_startwork' => $jobStartTimeCache ],
[
'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
wfGetLBFactory()->commitAll( __METHOD__ );
// 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->removeFfmpegLogFiles();
// Do a quick check to confirm the job was not restarted or removed while we were transcoding
// Confirm that the in memory $jobStartTimeCache matches db start time
$dbStartTime = $dbw->selectField( 'transcode', 'transcode_time_startwork',
[
'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() );
}
// Avoid "server has gone away" errors as copying can be slow
wfGetLBFactory()->commitAll( __METHOD__ );
// 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',
[
'transcode_error' => '',
'transcode_time_success' => $dbw->timestamp(),
'transcode_final_bitrate' => $bitrate
],
[
'transcode_image_name' => $this->getFile()->getName(),
'transcode_key' => $transcodeKey,
],
__METHOD__
);
// Commit to reduce contention
$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 );
$update = new CdnCacheUpdate( [ $url ] );
$update->doUpdate();
if ( $status !== true ) {
$this->setLastError( $status );
}
return $status === true;
}
function removeFfmpegLogFiles() {
$path = $this->getTargetEncodePath();
$dir = dirname( $path );
if ( is_dir( $dir ) ) {
$dh = opendir( $dir );
if ( $dh ) {
$file = readdir( $dh );
while ( $file !== 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();
}
$file = readdir( $dh );
}
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':
// @codingStandardsIgnoreStart
$cmd .= " -profile:v baseline -preset slow -coder 0 -bf 0 -weightb 1 -level 13 -maxrate 768k -bufsize 3M";
// @codingStandardsIgnoreEnd
break;
case '720p':
case 'ipod640':
// @codingStandardsIgnoreStart
$cmd .= " -profile:v baseline -preset slow -coder 0 -bf 0 -refs 1 -weightb 1 -level 31 -maxrate 10M -bufsize 10M";
// @codingStandardsIgnoreEnd
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 = [
'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 = [
'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 = [
"filesize" => $wgTranscodeBackgroundSizeLimit,
"memory" => $wgTranscodeBackgroundMemoryLimit,
"time" => $wgTranscodeBackgroundTimeLimit
];
return wfShellExec( $cmd . ' 2>&1', $retval, [], $limits,
[ '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 = [
"filesize" => $wgTranscodeBackgroundSizeLimit,
"memory" => $wgTranscodeBackgroundMemoryLimit,
"time" => $wgTranscodeBackgroundTimeLimit
];
$status = wfShellExec( $cmd . ' 2>&1', $retval, [], $limits,
[ '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();
// 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( $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 = [
// 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' => [ "--pp", "de" ],
'deinterlace' => "--deinterlace",
'novideo' => [ "--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"
];
}