View source
<?php
class video_ffmpeg implements transcoder_interface {
private $name = 'Locally Installed Transcoder (FFmpeg/Handbreke/Mcoder)';
private $value = 'video_ffmpeg';
protected $nice;
protected $thumbcmdoptions;
protected $enablefaststart;
protected $faststartcmd;
protected $cmdpath;
protected $logcommands;
public function __construct() {
$this->nice = variable_get('video_ffmpeg_nice_enable', FALSE) ? 'nice -n 19 ' : '';
$this->thumbcmdoptions = variable_get('video_ffmpeg_thumbnailer_options', '-i !videofile -an -y -f mjpeg -ss !seek -vframes 1 !thumbfile');
$this->enablefaststart = variable_get('video_ffmpeg_enable_faststart', FALSE);
$this->faststartcmd = variable_get('video_ffmpeg_faststart_cmd', '/usr/bin/qt-faststart');
$this->cmdpath = variable_get('video_transcoder_path', '/usr/bin/ffmpeg');
$this->logcommands = (bool) variable_get('video_ffmpeg_log_commands', TRUE);
}
private function run_command($command, &$output, $purpose = NULL, $ignoreoutputfilenotfound = FALSE) {
$output = '';
$command = $this->nice . $command . ' 2>&1';
$purposetext = !empty($purpose) ? ' ' . t('for') . ' ' . $purpose : '';
if ($this->logcommands) {
watchdog('video_ffmpeg', 'Executing ffmpeg command!purposetext: <pre>@command</pre>', array(
'@command' => $command,
'!purposetext' => $purposetext,
), 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_ffmpeg', 'Error executing ffmpeg 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($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 = $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_ffmpeg', $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($video) {
$this
->change_status($video->vid, VIDEO_RENDERING_ACTIVE);
$files = file_create_path();
$originaldir = $files . '/videos/original';
$converteddir = $files . '/videos/converted';
if (!field_file_check_directory($originaldir, FILE_CREATE_DIRECTORY)) {
watchdog('video_ffmpeg', 'Video conversion failed. Could not create directory %dir for storing original videos.', array(
'%dir' => $originaldir,
), WATCHDOG_ERROR);
return false;
}
if (!field_file_check_directory($converteddir, FILE_CREATE_DIRECTORY)) {
watchdog('video_ffmpeg', 'Video conversion failed. Could not create directory %dir for storing converted videos.', array(
'%dir' => $converteddir,
), WATCHDOG_ERROR);
return false;
}
$original = $originaldir . '/' . $video->filename;
$filepath = $video->filepath;
if (!file_move($filepath, $original)) {
watchdog('video_ffmpeg', 'Could not move video %orig to the original folder.', array(
'%orig' => $video->filepath,
), WATCHDOG_ERROR);
$this
->change_status($video->vid, VIDEO_RENDERING_FAILED);
return FALSE;
}
$video->filepath = $filepath;
drupal_write_record('files', $video, 'fid');
_video_db_increase_timeout();
$dimensions = $this
->dimensions($video);
$dimension = explode('x', $dimensions, 2);
$filepath = realpath($video->filepath);
$drupaldir = getcwd();
$tmpdir = tempnam(file_directory_temp(), 'ffmpeg-' . $video->fid);
unlink($tmpdir);
mkdir($tmpdir, 0777);
chdir($tmpdir);
$result = TRUE;
$presets = $video->presets;
$converted_files = array();
foreach ($presets as $presetname => $preset) {
$converted = file_create_filename(str_replace(' ', '_', pathinfo($video->filepath, PATHINFO_FILENAME)) . '.' . $preset['extension'], $converteddir);
$convertedfull = $drupaldir . '/' . $converted;
$ffmpeg_output = $convertedfull;
if ($this->enablefaststart && ($preset['extension'] == 'mp4' || $preset['extension'] == 'mov')) {
$ffmpeg_output = file_directory_temp() . '/' . basename($converted);
}
$command_output = '';
foreach ($preset['command'] as $command) {
$command = strtr($command, array(
'!cmd_path' => $this->cmdpath,
'!videofile' => escapeshellarg($filepath),
'!audiobitrate' => $preset['audio_bitrate'],
'!width' => intval($dimension[0]),
'!height' => intval($dimension[1]),
'!videobitrate' => $preset['video_bitrate'],
'!convertfile' => escapeshellarg($ffmpeg_output),
));
if (!$this
->run_command($command, $command_output, t('rendering preset %preset', array(
'%preset' => $presetname,
)))) {
$result = FALSE;
break 2;
}
}
if ($ffmpeg_output != $convertedfull && file_exists($ffmpeg_output)) {
$command_result = $this
->run_command($this->faststartcmd . ' ' . $ffmpeg_output . ' ' . $convertedfull, $command_output, t('running qt-faststart'));
file_delete($ffmpeg_output);
}
if (!file_exists($convertedfull) || ($filesize = filesize($convertedfull)) === 0) {
watchdog('video_ffmpeg', 'Video conversion failed for preset %preset: result file was not found.', array(
'%preset' => $presetname,
), WATCHDOG_ERROR);
$result = FALSE;
break;
}
$converted_files[] = $file = new stdClass();
$file->vid = intval($video->vid);
$file->filename = basename($converted);
$file->filepath = $converted;
$file->filemime = file_get_mimetype($converted);
$file->filesize = $filesize;
$file->preset = $presetname;
}
chdir($drupaldir);
rmdirr($tmpdir);
if ($result) {
$result = db_query('UPDATE {video_files} SET status = %d, completed = %d, data = "%s" WHERE vid = %d', VIDEO_RENDERING_COMPLETE, time(), serialize($converted_files), $video->vid);
$destinationfiles = array();
foreach ($converted_files as $file) {
$destinationfiles[] = $file->filepath;
}
watchdog('video_ffmpeg', '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);
}
$filepath = $video->filepath;
file_move($filepath, $files . '/videos');
$video->filepath = $filepath;
drupal_write_record('files', $video, 'fid');
$this
->change_status($video->vid, VIDEO_RENDERING_FAILED);
}
return $result;
}
private function get_video_info($video) {
$command = $this->cmdpath . ' -i ' . escapeshellarg($video);
$output = '';
if ($this
->run_command($command, $output, t('retrieving video info'), true)) {
return $output;
}
return NULL;
}
public function get_playtime($video) {
$ffmpeg_output = $this
->get_video_info($video);
$pattern = '/Duration: ([0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9])/';
preg_match_all($pattern, $ffmpeg_output, $matches, PREG_PATTERN_ORDER);
$playtime = $matches[1][0];
$hmsmm = explode(':', $playtime);
$tmp = explode('.', $hmsmm[2]);
$seconds = $tmp[0];
$hours = $hmsmm[0];
$minutes = $hmsmm[1];
return $seconds + $hours * 3600 + $minutes * 60;
}
public function get_dimensions($video) {
$ffmpeg_output = $this
->get_video_info($video);
$res = array(
'width' => 0,
'height' => 0,
);
$regex = ereg('[0-9]?[0-9][0-9][0-9]x[0-9][0-9][0-9][0-9]?', $ffmpeg_output, $regs);
if (isset($regs[0])) {
$dimensions = explode("x", $regs[0]);
$res['width'] = $dimensions[0] ? $dimensions[0] : NULL;
$res['height'] = $dimensions[1] ? $dimensions[1] : NULL;
}
return $res;
}
public function get_name() {
return $this->name;
}
public function get_value() {
return $this->value;
}
public function get_help() {
return l(t('FFMPEG Online Manual'), 'http://www.ffmpeg.org/');
}
public function admin_settings() {
$form = array();
$form['video_ffmpeg_start'] = array(
'#type' => 'markup',
'#value' => '<div id="video_ffmpeg">',
);
$form['video_transcoder_path'] = array(
'#type' => 'textfield',
'#title' => t('Path to Video Transcoder'),
'#description' => t('Absolute path to ffmpeg.'),
'#default_value' => $this->cmdpath,
);
$form['video_thumbs'] = array(
'#type' => 'textfield',
'#title' => t('Number of thumbnails'),
'#description' => t('Number of thumbnails to display from video.'),
'#default_value' => variable_get('video_thumbs', 5),
);
$form['video_ffmpeg_nice_enable'] = array(
'#type' => 'checkbox',
'#title' => t('Enable the use of nice when calling the ffmpeg command.'),
'#default_value' => variable_get('video_ffmpeg_nice_enable', TRUE),
'#description' => t('The nice command Invokes a command with an altered scheduling priority. This option may not be available on windows machines, so disable it.'),
);
$form['video_ffmpeg_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 ffmpeg encoding to log all commands to the <a href="@dblog-page">Drupal log</a>. This may help with debugging ffmpeg 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('Video 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.'),
'#default_value' => variable_get('video_thumb_path', 'video_thumbs'),
);
$form['autothumb']['advanced'] = array(
'#type' => 'fieldset',
'#title' => t('Advanced Settings'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['autothumb']['advanced']['video_ffmpeg_thumbnailer_options'] = array(
'#type' => 'textarea',
'#title' => t('Video thumbnailer options'),
'#description' => t('Provide the options for the thumbnailer. Available argument values are: ') . '<ol><li>' . t('!videofile (the video file to thumbnail)') . '<li>' . t('!thumbfile (a newly created temporary file to overwrite with the thumbnail)</ol>'),
'#default_value' => $this->thumbcmdoptions,
'#wysiwyg' => FALSE,
);
$form['autoconv'] = array(
'#type' => 'fieldset',
'#title' => t('Video Conversion'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['autoconv']['video_ffmpeg_enable_faststart'] = array(
'#type' => 'checkbox',
'#title' => t('Process mov/mp4 videos with qt-faststart'),
'#default_value' => $this->enablefaststart,
);
$form['autoconv']['video_ffmpeg_faststart_cmd'] = array(
'#type' => 'textfield',
'#title' => t('Path to qt-faststart'),
'#default_value' => $this->faststartcmd,
);
$form['autoconv']['video_ffmpeg_pad_method'] = array(
'#type' => 'radios',
'#title' => t('FFmpeg Padding method'),
'#default_value' => variable_get('video_ffmpeg_pad_method', 0),
'#options' => array(
0 => t('Use -padtop, -padbottom, -padleft, -padright for padding'),
1 => t('Use -vf "pad=w:h:x:y:c" for padding'),
),
);
$form['video_ffmpeg_end'] = array(
'#type' => 'markup',
'#value' => '</div>',
);
return $form;
}
public function admin_settings_validate($form, &$form_state) {
return;
}
public function create_job($video) {
if (empty($video['dimensions'])) {
watchdog('video_ffmpeg', 'Tried to create ffmpeg job for video %video with empty dimensions value.', array(
'%video' => $video['fid'],
), WATCHDOG_ERROR);
return FALSE;
}
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($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($video) {
if (!$this
->load_job($video->fid)) {
return;
}
$sql = db_query("SELECT data FROM {video_files} WHERE fid=%d", $video->fid);
while ($row = db_fetch_object($sql)) {
$data = unserialize($row->data);
if (empty($data)) {
continue;
}
foreach ($data as $file) {
if (file_exists($file->filepath)) {
unlink($file->filepath);
}
}
}
db_query('DELETE FROM {video_files} WHERE fid = %d', $video->fid);
}
public function load_job($fid) {
$job = null;
$result = db_query('SELECT f.*, vf.vid, vf.nid, vf.dimensions, vf.status as video_status FROM {video_files} vf LEFT JOIN {files} f ON vf.fid = f.fid WHERE f.fid=vf.fid AND f.fid = %d', $fid);
$job = db_fetch_object($result);
if (!empty($job)) {
return $job;
}
else {
return FALSE;
}
}
public function load_job_queue() {
$total_videos = variable_get('video_ffmpeg_instances', 5);
$videos = array();
$result = db_query_range('SELECT f.*, vf.vid, vf.nid, vf.dimensions, vf.status as video_status 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, $total_videos);
while ($row = db_fetch_object($result)) {
$videos[] = $row;
}
return $videos;
}
public function load_completed_job(&$video) {
$result = db_fetch_object(db_query('SELECT * FROM {video_files} WHERE fid = %d', $video->fid));
$data = unserialize($result->data);
if (empty($data)) {
return $video;
}
foreach ($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 = strtolower($extension);
}
return $video;
}
public function change_status($vid, $status) {
$result = db_query('UPDATE {video_files} SET status = %d WHERE vid = %d ', $status, $vid);
}
public function dimensions($video) {
$aspect_ratio = _video_aspect_ratio($video->filepath);
$ratio = $aspect_ratio['ratio'];
$width = $aspect_ratio['width'];
$height = $aspect_ratio['height'];
$wxh = explode('x', $video->dimensions);
$output_width = $wxh[0];
$output_height = $wxh[1];
$output_ratio = number_format($output_width / $output_height, 4);
if ($output_ratio != $ratio && $width && $height) {
$options = array();
if ($ratio < $output_width / $output_height) {
$end_width = $output_height * $ratio;
$end_height = $output_height;
}
else {
$end_height = $output_width / $ratio;
$end_width = $output_width;
}
if ($end_width == $output_width) {
$padding = round($output_height - $end_height);
$pad1 = $pad2 = floor($padding / 2);
if ($pad1 % 2 !== 0) {
$pad1++;
$pad2--;
}
if (variable_get('video_ffmpeg_pad_method', 0)) {
$options[] = '-vf "pad=' . round($output_width) . ':' . round($output_height) . ':0:' . $pad1 . '"';
}
else {
$options[] = '-padtop ' . $pad1;
$options[] = '-padbottom ' . $pad2;
}
}
else {
$padding = round($output_width - $end_width);
$pad1 = $pad2 = floor($padding / 2);
if ($pad1 % 2 !== 0) {
$pad1++;
$pad2--;
}
if (variable_get('video_ffmpeg_pad_method', 0)) {
$options[] = '-vf "pad=' . round($output_width) . ':' . round($output_height) . ':' . $pad1 . ':0"';
}
else {
$options[] = '-padleft ' . $pad1;
$options[] = '-padright ' . $pad2;
}
}
$end_width = round($end_width) % 2 !== 0 ? round($end_width) + 1 : round($end_width);
$end_height = round($end_height) % 2 !== 0 ? round($end_height) + 1 : round($end_height);
array_unshift($options, $end_width . 'x' . $end_height);
return implode(' ', $options);
}
else {
return $video->dimensions;
}
}
}