View source
<?php
$plugin = array();
class video_localcommand implements video_transcoder {
private $name = 'Locally Installed Transcoder (FFmpeg/Handbreke/Mcoder)';
private $value = 'video_localcommand';
protected $usenice;
protected $thumbcmdoptions;
protected $faststartcmd;
protected $flvtoolcmd;
protected $cmdpath;
protected $logcommands;
public function __construct() {
$this->usenice = PHP_OS == 'Linux' && variable_get('video_localcommand_nice_enable', FALSE);
$this->thumbcmdoptions = variable_get('video_localcommand_thumbnailer_options', '-i !videofile -an -y -f mjpeg -ss !seek -vframes 1 !thumbfile');
$this->faststartcmd = variable_get('video_localcommand_qtfaststart_cmd', '/usr/bin/qt-faststart');
$this->flvtoolcmd = variable_get('video_flvtool2_path', '/usr/bin/flvtool2');
$this->cmdpath = variable_get('video_transcoder_path', '/usr/bin/ffmpeg');
$this->logcommands = (bool) variable_get('video_localcommand_log_commands', TRUE);
}
private function run_command($command, &$output, $purpose = NULL, $ignoreoutputfilenotfound = FALSE) {
$output = '';
$command = $command . ' 2>&1';
if ($this->usenice) {
$command = 'nice -n 19 ' . $command;
}
$purposetext = !empty($purpose) ? ' ' . t('for') . ' ' . $purpose : '';
if ($this->logcommands) {
watchdog('video_command', 'Executing command!purposetext: <pre>@command</pre>', array(
'@command' => $command,
'!purposetext' => !empty($purpose) ? ' for ' . $purpose : '',
), WATCHDOG_DEBUG);
}
$return_var = 0;
ob_start();
passthru($command, $return_var);
$output = ob_get_clean();
if ($return_var != 0 && ($return_var != 1 || !$ignoreoutputfilenotfound)) {
watchdog('video_command', 'Error executing command!purposetext:<br/><pre>@command</pre>Output:<br/><pre>@output</pre>', array(
'@command' => $command,
'@output' => trim($output),
'!purposetext' => $purposetext,
), WATCHDOG_ERROR);
return FALSE;
}
return TRUE;
}
public function generate_thumbnails(stdClass $video) {
global $user;
$final_thumb_path = video_thumb_path($video);
$total_thumbs = variable_get('video_thumbs', 5);
$files = NULL;
for ($i = 1; $i <= $total_thumbs; $i++) {
$filename = '/video-thumb-for-' . $video->fid . '-' . $i . '.jpg';
$thumbfile = $final_thumb_path . $filename;
if (!is_file($thumbfile)) {
if (!isset($duration)) {
$duration = $this
->get_playtime($video->filepath);
}
$seek = floor($duration / $total_thumbs) * $i - 1;
$command = $this->cmdpath . ' ' . strtr($this->thumbcmdoptions, array(
'!videofile' => escapeshellarg($video->filepath),
'!seek' => $seek,
'!thumbfile' => $thumbfile,
));
$command_output = '';
$this
->run_command($command, $command_output, t('generating thumbnails'));
$exists = file_exists($thumbfile);
if (!$exists || filesize($thumbfile) == 0) {
$params = array(
'%file' => $thumbfile,
'%video' => $video->filename,
);
if ($exists) {
$error_msg = 'Error generating thumbnail for video %video: generated file %file is empty. This problem may be caused by a broken video file. The video reports that its length is @duration seconds. If this is wrong, please recreate the video and try again.';
$params['@duration'] = $duration;
unlink($thumbfile);
}
else {
$error_msg = 'Error generating thumbnail for video %video: generated file %file does not exist.';
}
watchdog('video_command', $error_msg, $params, WATCHDOG_ERROR);
drupal_set_message(t($error_msg, $params), 'error');
break;
}
}
$file = new stdClass();
$file->uid = $user->uid;
$file->status = FILE_STATUS_TEMPORARY;
$file->filename = $filename;
$file->filepath = $thumbfile;
$file->filemime = 'image/jpeg';
$file->filesize = filesize($thumbfile);
$file->timestamp = time();
$files[] = $file;
}
return $files;
}
public function convert_video(stdClass $video) {
$this
->change_status($video, VIDEO_RENDERING_ACTIVE);
$converteddir = dirname(dirname($video->filepath)) . '/converted';
$converteddirabsolute = FALSE;
if ($converteddir[0] == '/') {
$converteddirabsolute = TRUE;
}
elseif (substr($converteddir, 1, 2) == ':\\') {
$converteddirabsolute = TRUE;
}
if (!field_file_check_directory($converteddir, FILE_CREATE_DIRECTORY)) {
watchdog('video_command', 'Video conversion could not be started: Could not create directory %dir for storing converted videos.', array(
'%dir' => $converteddir,
), WATCHDOG_ERROR);
return FALSE;
}
$sourcedimensions = $this
->get_dimensions($video->filepath);
if (empty($sourcedimensions)) {
watchdog('video_command', 'Video conversion could not be started: Could not find file information for file %file.', array(
'%file' => $video->filepath,
), WATCHDOG_ERROR);
return FALSE;
}
_video_db_increase_timeout();
$dimensionparameters = $this
->getDimensionParameters($video, $sourcedimensions);
$parameters = array(
'!cmd_path' => $this->cmdpath,
'!videofile' => escapeshellarg(realpath($video->filepath)),
'!convertfile' => NULL,
'!width' => $dimensionparameters->width,
'!height' => $dimensionparameters->height,
'!paddingwidth' => $dimensionparameters->paddingwidth,
'!paddingheight' => $dimensionparameters->paddingheight,
'!paddingtop' => $dimensionparameters->paddingtop,
'!paddingbottom' => $dimensionparameters->paddingbottom,
'!paddingleft' => $dimensionparameters->paddingleft,
'!paddingright' => $dimensionparameters->paddingright,
);
$drupaldir = getcwd();
$tmpdir = tempnam(file_directory_temp(), 'drupal-video-' . $video->fid);
unlink($tmpdir);
mkdir($tmpdir, 0777);
chdir($tmpdir);
$result = TRUE;
$converted_files = array();
foreach ($video->presets as $preset) {
$settings = $preset
->getSettings();
$outputfile = file_create_filename(str_replace(' ', '_', pathinfo($video->filepath, PATHINFO_FILENAME)) . $preset->filenamesuffix . '.' . $preset->extension, $converteddir);
$outputfilefull = $converteddirabsolute ? $outputfile : $drupaldir . '/' . $outputfile;
$parameters['!convertfile'] = escapeshellarg($outputfilefull);
$command_output = '';
foreach ($settings['commands'] as $cmdnr => $command) {
$command = strtr($command, $parameters);
if (!$this
->run_command($command, $command_output, t('rendering preset %preset, command #@cmdnr', array(
'%preset' => $preset->name,
'@cmdnr' => $cmdnr + 1,
)))) {
$result = FALSE;
break 2;
}
}
if (!empty($settings['useflvtool2'])) {
$command = $this->flvtoolcmd . ' -U ' . escapeshellarg($outputfilefull);
$this
->run_command($command, $command_output, t('running flvtool2'));
}
if (!empty($settings['useqtfaststart'])) {
$tmpfile = $outputfilefull . '-qt';
$command = $this->faststartcmd . ' ' . escapeshellarg($outputfilefull) . ' ' . escapeshellarg($tmpfile);
if ($this
->run_command($command, $command_output, t('running qt-faststart')) && file_exists($tmpfile)) {
file_delete($outputfilefull);
rename($tmpfile, $outputfilefull);
}
else {
file_delete($tmpfile);
}
}
if (!file_exists($outputfilefull) || ($filesize = filesize($outputfilefull)) === 0) {
watchdog('video_command', 'Video conversion failed for preset %preset: result file was not found.', array(
'%preset' => $preset->name,
), WATCHDOG_ERROR);
$result = FALSE;
break;
}
$converted_files[] = $file = new stdClass();
$file->vid = intval($video->vid);
$file->filename = basename($outputfile);
$file->filepath = $outputfile;
$file->filemime = file_get_mimetype($outputfile);
$file->filesize = $filesize;
$file->preset = $preset->name;
}
chdir($drupaldir);
rmdirr($tmpdir);
if ($result) {
$video->status = VIDEO_RENDERING_COMPLETE;
$video->completed = time();
$video->data = $converted_files;
$result = db_query('UPDATE {video_files} SET status = %d, completed = %d, data = "%s" WHERE vid = %d', $video->status, $video->completed, serialize($video->data), $video->vid);
$destinationfiles = array();
foreach ($converted_files as $file) {
$destinationfiles[] = $file->filepath;
}
watchdog('video_command', 'Successfully converted %orig to !destination-files', array(
'%orig' => $video->filepath,
'!destination-files' => implode(', ', $destinationfiles),
), WATCHDOG_INFO);
}
else {
foreach ($converted_files as $file) {
file_delete($file->filepath);
}
$this
->change_status($video, VIDEO_RENDERING_FAILED);
}
return $result;
}
private function get_video_info($filepath) {
$command = $this->cmdpath . ' -i ' . escapeshellarg($filepath);
$output = '';
if ($this
->run_command($command, $output, t('retrieving video info'), TRUE)) {
return $output;
}
return NULL;
}
public function get_playtime($filepath) {
$output = $this
->get_video_info($filepath);
$match = array();
if (preg_match('/Duration: ([0-9]{2}):([0-9]{2}):([0-9]{2})\\.([0-9])/', $output, $match)) {
$hours = $match[1];
$minutes = $match[2];
$seconds = $match[3];
if ($match[4] >= 5) {
$seconds++;
}
return $seconds + $hours * 3600 + $minutes * 60;
}
return NULL;
}
public function get_dimensions($filepath) {
$output = $this
->get_video_info($filepath);
$match = array();
if (preg_match('#(\\d{2,4})x(\\d{2,4})#', $output, $match)) {
return array(
'width' => intval($match[1]),
'height' => intval($match[2]),
);
}
return NULL;
}
public function get_name() {
return $this->name;
}
public function get_value() {
return $this->value;
}
public function create_job(stdClass $video) {
$video->status = VIDEO_RENDERING_PENDING;
return db_query('INSERT INTO {video_files} (fid, status, dimensions) VALUES (%d, %d, "%s")', $video->fid, VIDEO_RENDERING_PENDING, $video->dimensions);
}
public function update_job(stdClass $video) {
if (!$this
->load_job($video->fid)) {
return;
}
db_query('UPDATE {video_files} SET nid = %d WHERE fid=%d', $video->nid, $video->fid);
}
public function delete_job(stdClass $video) {
db_query('DELETE FROM {video_files} WHERE fid = %d', $video->fid);
}
public function load_job($fid) {
$job = db_fetch_object(db_query('SELECT f.fid, f.filename, f.filepath, f.filesize, f.filemime, vf.vid, vf.nid, vf.dimensions, vf.status, vf.data FROM {video_files} vf LEFT JOIN {files} f ON vf.fid = f.fid WHERE f.fid=vf.fid AND f.fid = %d', $fid));
if (!empty($job)) {
$job->data = unserialize($job->data);
return $job;
}
return FALSE;
}
public function load_job_queue($num) {
$videos = array();
$result = db_query_range('SELECT f.fid, f.filename, f.filepath, f.filemime, f.filesize, vf.vid, vf.nid, vf.dimensions, vf.status, vf.data FROM {video_files} vf LEFT JOIN {files} f ON vf.fid = f.fid WHERE vf.status = %d AND f.status = %d ORDER BY f.timestamp', VIDEO_RENDERING_PENDING, FILE_STATUS_PERMANENT, 0, $num);
while ($row = db_fetch_object($result)) {
$row->data = unserialize($row->data);
$videos[] = $row;
}
return $videos;
}
public function load_completed_job(stdClass $video) {
$result = db_fetch_object(db_query('SELECT data FROM {video_files} WHERE fid = %d', $video->fid));
foreach (unserialize($result->data) as $value) {
$extension = pathinfo($value->filepath, PATHINFO_EXTENSION);
$video->files->{$extension} = new stdClass();
$video->files->{$extension}->filename = pathinfo($value->filepath, PATHINFO_FILENAME) . '.' . $extension;
$video->files->{$extension}->filepath = $value->filepath;
$video->files->{$extension}->url = file_create_url($value->filepath);
$video->files->{$extension}->extension = $extension;
$video->files->{$extension}->filemime = file_get_mimetype($value->filepath);
$video->player = drupal_strtolower($extension);
}
return $video;
}
public function change_status(stdClass $video, $status) {
$video->status = $status;
db_query('UPDATE {video_files} SET status = %d WHERE vid = %d', $status, $video->vid);
}
public function getDimensionParameters(stdClass $video, array $sourcedimensions) {
$wxh = explode('x', $video->dimensions, 2);
$outputwidth = intval($wxh[0]);
$outputheight = intval($wxh[1]);
$outputratio = _video_aspect_ratio($outputwidth, $outputheight);
$options = new stdClass();
$options->paddingleft = 0;
$options->paddingright = 0;
$options->paddingtop = 0;
$options->paddingbottom = 0;
$options->width = intval(round($outputwidth / 2) * 2);
$options->height = intval(round($outputheight / 2) * 2);
$options->paddingwidth = $options->width;
$options->paddingheight = $options->height;
$filewidth = $sourcedimensions['width'];
$fileheight = $sourcedimensions['height'];
$fileratio = _video_aspect_ratio($filewidth, $fileheight);
if ($outputratio != $fileratio && $filewidth && $fileheight) {
if ($fileratio < $outputratio) {
$options->width = intval(round($filewidth * $outputheight / $fileheight / 2) * 2);
$padding = $outputwidth - $options->width;
$options->paddingleft = intval(floor($padding / 2));
$options->paddingright = intval(ceil($padding / 2));
}
else {
$options->height = intval(round($fileheight * $outputwidth / $filewidth / 2) * 2);
$padding = $outputheight - $options->height;
$options->paddingtop = intval(floor($padding / 2));
$options->paddingbottom = intval(ceil($padding / 2));
}
}
return $options;
}
public function get_help() {
return l(t('FFmpeg Online Manual'), 'http://www.ffmpeg.org/');
}
public function admin_settings(&$form_state) {
$form = array();
$form['video_localcommand_start'] = array(
'#type' => 'markup',
'#value' => '<div id="video_localcommand">',
);
$form['video_transcoder_path'] = array(
'#type' => 'textfield',
'#title' => t('Path to transcoder executable'),
'#description' => t('Absolute path to your transcoder, such as FFmpeg. Do not include parameters.'),
'#default_value' => $this->cmdpath,
);
$form['video_thumbs'] = array(
'#type' => 'textfield',
'#title' => t('Number of thumbnails'),
'#description' => t('Number of thumbnails to generate for videos.'),
'#default_value' => variable_get('video_thumbs', 5),
);
$form['video_localcommand_nice_enable'] = array(
'#type' => 'checkbox',
'#title' => t('Enable the use of nice when calling the command.'),
'#default_value' => $this->usenice,
'#description' => t('The nice command invokes a command with an altered scheduling priority.'),
'#access' => PHP_OS == 'Linux',
);
$form['video_localcommand_log_commands'] = array(
'#type' => 'checkbox',
'#title' => t('Log all executed commands to the Drupal log.'),
'#default_value' => $this->logcommands,
'#description' => t('Enable this option when debugging local transcoding to log all commands to the <a href="@dblog-page">Drupal log</a>. This may help with debugging problems. When this option is disabled, only errors will be logged.', array(
'@dblog-page' => url('admin/reports/dblog'),
)),
);
$form['autothumb'] = array(
'#type' => 'fieldset',
'#title' => t('Thumbnails'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['autothumb']['video_thumb_path'] = array(
'#type' => 'textfield',
'#title' => t('Path to save thumbnails'),
'#description' => t('Path to save video thumbnails extracted from the videos.') . ' ' . t('This path will be created within the %files-directory directory.', array(
'%files-directory' => file_directory_path(),
)),
'#default_value' => variable_get('video_thumb_path', 'video_thumbs'),
);
$form['autothumb']['video_localcommand_thumbnailer_options'] = array(
'#type' => 'textarea',
'#title' => t('Video thumbnailer options'),
'#description' => t('Provide the options for the thumbnailer. Available argument values are:') . theme('item_list', array(
'<strong>!seek</strong>: ' . t('the number of seconds into the video where the screenshot is taken'),
'<strong>!videofile</strong>: ' . t('the video file to create thumbnails for'),
'<strong>!thumbfile</strong>: ' . t('a newly created temporary file to overwrite with the thumbnail'),
)),
'#default_value' => $this->thumbcmdoptions,
'#wysiwyg' => FALSE,
);
$form['helpers'] = array(
'#type' => 'fieldset',
'#title' => t('Helper programs'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['helpers']['video_localcommand_qtfaststart_cmd'] = array(
'#type' => 'textfield',
'#title' => t('Path to qt-faststart'),
'#default_value' => $this->faststartcmd,
'#description' => t('When you enter the path to qt-faststart here, you can enable the qt-faststart tool for one or more presets.'),
);
$form['helpers']['video_flvtool2_path'] = array(
'#type' => 'textfield',
'#title' => t('Path to flvtool2'),
'#default_value' => $this->flvtoolcmd,
'#description' => t('When you enter the path to flvtool2 here, you can enable the flvtool2 tool for one or more presets.'),
);
$form['video_localcommand_end'] = array(
'#type' => 'markup',
'#value' => '</div>',
);
return $form;
}
public function admin_settings_validate($form, &$form_state) {
$v =& $form_state['values'];
if (!empty($v['video_transcoder_path'])) {
if (!is_file($v['video_transcoder_path'])) {
form_error($form['video_transcoder_path'], t('The file %file does not exist.', array(
'%file' => $v['video_transcoder_path'],
)));
}
}
elseif ($v['vid_convertor'] == 'video_localcommand') {
form_error($form['video_transcoder_path'], t('!name field is required.', array(
'!name' => $form['video_transcoder_path']['#title'],
)));
}
if (!empty($v['video_localcommand_qtfaststart_cmd'])) {
if (!is_file($v['video_localcommand_qtfaststart_cmd'])) {
form_error($form['helpers']['video_localcommand_qtfaststart_cmd'], t('The file %file does not exist.', array(
'%file' => $v['video_localcommand_qtfaststart_cmd'],
)));
}
}
if (!empty($v['video_flvtool2_path'])) {
if (!is_file($v['video_flvtool2_path'])) {
form_error($form['helpers']['video_flvtool2_path'], t('The file %file does not exist.', array(
'%file' => $v['video_flvtool2_path'],
)));
}
}
}
public function preset_settings(&$form_state, video_preset $preset) {
$form = array();
$settings = $preset
->getSettings();
if (!isset($settings['commands'])) {
$settings['commands'] = array(
'',
);
}
$settings['commands'][] = '';
$form['commands'] = array(
'#tree' => TRUE,
);
foreach ($settings['commands'] as $k => $command) {
$form['commands'][$k] = array(
'#type' => 'textarea',
'#title' => t('Command @num', array(
'@num' => $k + 1,
)),
'#default_value' => $command,
'#rows' => 2,
'#required' => $k === 0,
'#wysiwyg' => FALSE,
);
}
if (!empty($this->flvtoolcmd)) {
$form['useflvtool2'] = array(
'#type' => 'checkbox',
'#title' => t('Post-process the output with flvtool2'),
'#default_value' => !empty($settings['useflvtool2']),
);
}
if (!empty($this->faststartcmd)) {
$form['useqtfaststart'] = array(
'#type' => 'checkbox',
'#title' => t('Post-process the output with qt-faststart'),
'#default_value' => !empty($settings['useqtfaststart']),
);
}
$form['help'] = array(
'#type' => 'item',
'#title' => t('Notes'),
'#value' => '<p>' . t('The commands are executed in a temporary directory that is removed after the conversion has completed.') . '<br/>' . t('If you are using FFmpeg to transcode large videos, consider adding <code>-nostats</code> to your command line(s) to reduce memory usage while transcoding.') . '<br/>' . t('Submit this form and revisit this page to add more commands.') . '</p>' . '<p>' . t('You can use the following replacement variables in your commands:') . '</p>' . theme('item_list', array(
'<strong>!cmd_path</strong>: ' . t('the path to your executable (currently: %executable).', array(
'%executable' => $this->cmdpath,
)),
'<strong>!videofile</strong>: ' . t('the escaped absolute path to the input video file.'),
'<strong>!convertfile</strong>: ' . t('the escaped absolute path to the output video file.'),
'<strong>!width</strong>: ' . t('the width of the output video file, excluding padding, for use with -s.'),
'<strong>!height</strong>: ' . t('the height of the output video file, excluding padding, for use with -s.'),
'<strong>!paddingwidth</strong>: ' . t('the width of the output video file, including padding, for use as first parameter of -vf pad.'),
'<strong>!paddingheight</strong>: ' . t('the height of the output video file, including padding, for use as second parameter of -vf pad.'),
'<strong>!paddingtop</strong>: ' . t('the padding added to the top of the output video file, for use with -padtop or as third parameter of -vf pad.'),
'<strong>!paddingbottom</strong>: ' . t('the padding added to the bottom of the output video file, for use with -padbottom.'),
'<strong>!paddingleft</strong>: ' . t('the padding added to the left of the output video file, for use with -padleft or as fourth parameter of -vf pad.'),
'<strong>!paddingright</strong>: ' . t('the padding added to the right of the output video file, for use with -padright.'),
)),
);
return $form;
}
public function preset_settings_validate($form, &$form_state) {
if (!empty($form_state['values']['commands'])) {
foreach ($form_state['values']['commands'] as $k => $v) {
if (empty($v)) {
unset($form_state['values']['commands'][$k]);
}
}
$form_state['values']['commands'] = array_values($form_state['values']['commands']);
}
if (!empty($form['useflvtool2'])) {
$form['useflvtool2'] = TRUE;
}
if (!empty($form['useqtfaststart'])) {
$form['useqtfaststart'] = TRUE;
}
}
public function requirements() {
$requirements = array();
$t = get_t();
if (strpos($this->cmdpath, ' -') !== FALSE) {
$requirements['video_localcommand_args'] = array(
'title' => $t('Video local command'),
'description' => $t('It is no longer necessary to add command line arguments to the %setting setting. You can add command line arguments per preset.', array(
'%setting' => t('Path to transcoder executable'),
)),
'value' => l($t('Change setting'), 'admin/settings/video/transcoders'),
'severity' => REQUIREMENT_WARNING,
);
}
elseif (!file_exists($this->cmdpath)) {
$requirements['video_localcommand_args'] = array(
'title' => $t('Video local command'),
'description' => $t('The file %cmdpath can\'t be found. Please change your settings.', array(
'%cmdpath' => $this->cmdpath,
)),
'value' => l($t('Change setting'), 'admin/settings/video/transcoders'),
'severity' => REQUIREMENT_ERROR,
);
}
elseif (!is_executable($this->cmdpath)) {
$requirements['video_localcommand_args'] = array(
'title' => $t('Video local command'),
'description' => $t('The file %cmdpath is not an executable. Please change your settings.', array(
'%cmdpath' => $this->cmdpath,
)),
'value' => l($t('Change setting'), 'admin/settings/video/transcoders'),
'severity' => REQUIREMENT_ERROR,
);
}
return $requirements;
}
public function get_original_path_by_converted_path($filepath) {
$videos = db_query('SELECT fid, data FROM {video_files} WHERE status = %d AND data LIKE "%%%s%%"', VIDEO_RENDERING_COMPLETE, $filepath);
$fid = NULL;
while ($video = db_fetch_object($videos)) {
foreach (unserialize($video->data) as $file) {
if ($file->filepath == $filepath) {
$fid = $video->fid;
break 2;
}
}
}
if ($fid == NULL) {
return NULL;
}
return $this
->load_job($fid);
}
}