View source
<?php
class video_ffmpeg implements transcoder_interface {
private $name = 'Locally Installed Transcoders (FFMPEG/Handbreke/Mcoder)';
private $value = 'video_ffmpeg';
protected $params = array();
protected $thumb_command = '-i !videofile -an -y -f mjpeg -ss !seek -vframes 1 !thumbfile';
protected $nice;
public function __construct() {
$this->params['ffmpeg'] = variable_get('video_ffmpeg_path', '/usr/bin/ffmpeg');
$this->params['ffmpeg2theora'] = variable_get('video_ffmpeg2theora_path', '/usr/bin/ffmpeg2theora');
$this->params['mcoder'] = variable_get('video_mcoder_path', '/usr/bin/mcoder');
$this->params['handbreke'] = variable_get('video_handbreke_path', '/usr/bin/HandBrakeCLI');
$this->params['other'] = variable_get('video_other_path', '');
$this->params['thumb_command'] = variable_get('video_ffmpeg_thumbnailer_options', $this->thumb_command);
$this->nice = variable_get('video_ffmpeg_nice_enable', FALSE) ? 'nice -n 19 ' : '';
$this->params['enable_faststart'] = variable_get('video_ffmpeg_enable_faststart', 0);
$this->params['faststart_cmd'] = variable_get('video_ffmpeg_faststart_cmd', '/usr/bin/qt-faststart');
}
public function run_command($command) {
$command = strtr($command, array(
'!ffmpeg' => $this->params['ffmpeg'],
'!ffmpeg2theora' => $this->params['ffmpeg2theora'],
'!mcoder' => $this->params['mcoder'],
'!handbreke' => $this->params['handbreke'],
'!other' => $this->params['other'],
));
$command = $this->nice . $command . ' 2>&1';
watchdog('transcoder', 'Executing command: ' . $command, array(), WATCHDOG_DEBUG);
$command_return = shell_exec($command);
return $command_return;
}
public function generate_thumbnails($video) {
global $user;
$video_thumb_path = variable_get('video_thumb_path', 'videos/thumbnails');
$schema_thumb_path = file_default_scheme() . '://' . $video_thumb_path . '/' . $video['fid'];
file_prepare_directory($schema_thumb_path, FILE_CREATE_DIRECTORY);
$total_thumbs = variable_get('video_thumbs', 5);
$videofile = file_load($video['fid']);
$videopath = drupal_realpath($videofile->uri);
$duration = $this
->get_playtime($videopath);
$files = NULL;
for ($i = 1; $i <= $total_thumbs; $i++) {
$seek = $duration / $total_thumbs * $i - 1;
$filename = file_munge_filename("video-thumb-" . $video['fid'] . "-{$i}.jpg", '', TRUE);
$thumbfile = $schema_thumb_path . '/' . $filename;
if (!is_file(drupal_realpath($thumbfile))) {
$command = strtr($this->params['thumb_command'], array(
'!videofile' => '"' . $videopath . '"',
'!seek' => $seek,
'!thumbfile' => '"' . drupal_realpath($thumbfile) . '"',
));
$command_output = $this
->run_command($command);
if (!file_exists(drupal_realpath($thumbfile))) {
$error_param = array(
'%file' => $thumbfile,
'%cmd' => $command,
'%out' => $command_output,
);
$error_msg = t("Error generating thumbnail for video: generated file %file does not exist.<br />Command Executed:<br />%cmd<br />Command Output:<br />%out", $error_param);
watchdog('transcoder', $error_msg, array(), WATCHDOG_ERROR);
continue;
}
}
$file = new stdClass();
$file->uid = $user->uid;
$file->status = 0;
$file->filename = trim($filename);
$file->uri = $thumbfile;
$file->filemime = file_get_mimetype($filename);
$file->filesize = filesize(drupal_realpath($thumbfile));
$file->timestamp = time();
$files[] = $file;
}
return $files;
}
public function get_codecs() {
$codecs = array(
'encode' => array(
'video' => array(
'h264' => 'H.264 (default)',
'vp8' => 'VP8',
'theora' => 'Theora',
'vp6' => 'VP6',
'mpeg4' => 'MPEG-4',
'wmv' => 'WMV',
),
'audio' => array(
'aac' => 'AAC (default for most cases)',
'mp3' => 'MP3',
'vorbis' => 'Vorbis (default for VP8 and Theora)',
'wma' => 'WMA',
),
),
'decoding' => array(),
);
return $codecs;
}
public function convert_video($video) {
$target = str_replace('original', '', drupal_dirname($video->uri));
$converted_base_dir = $target . 'converted/' . $video->fid;
if (!file_prepare_directory($converted_base_dir, FILE_CREATE_DIRECTORY)) {
watchdog('transcoder', 'Video conversion failed. Could not create the directory: ' . $converted_base_dir, array(), WATCHDOG_ERROR);
return FALSE;
}
$original_video_path = drupal_realpath($video->uri);
$presets = $video->presets;
$converted_files = array();
foreach ($presets as $name => $preset) {
$settings = $preset['settings'];
if (isset($settings['width']) && !empty($settings['width']) && isset($settings['height']) && !empty($settings['height']) && variable_get('video_use_preset_wxh', FALSE)) {
$video->dimensions = $settings['width'] . 'x' . $settings['height'];
}
$converted = $converted_base_dir . '/' . file_munge_filename(str_replace(' ', '_', pathinfo($original_video_path, PATHINFO_FILENAME)) . '_' . strtolower($name) . '.' . $settings['video_extension'], $settings['video_extension']);
$converted_video_path = drupal_realpath($converted);
$dimensions = $this
->dimensions($video);
$dimention = explode('x', $dimensions);
if ($this->params['enable_faststart'] && in_array($settings['video_extension'], array(
'mov',
'mp4',
))) {
$ffmpeg_output = file_directory_temp() . '/' . basename($converted_video_path);
}
else {
$ffmpeg_output = $converted_video_path;
}
$command = strtr($settings['cli_code'], array(
'!videofile' => '"' . $original_video_path . '"',
'!audiobitrate' => $settings['audio_bitrate'],
'!width' => $dimention[0],
'!height' => $dimention[1],
'!videobitrate' => $settings['video_bitrate'],
'!convertfile' => '"' . $ffmpeg_output . '"',
));
$command_output = $this
->run_command($command);
if ($ffmpeg_output != $converted_video_path && file_exists($ffmpeg_output)) {
$command = implode(' ', array(
$this->params['faststart_cmd'],
$ffmpeg_output,
$converted_video_path,
));
$command_output .= $this
->run_command($command);
drupal_unlink($ffmpeg_output);
}
if (!file_exists($converted_video_path) || !filesize($converted_video_path)) {
watchdog('transcoder', 'Video conversion failed for preset %preset. FFMPEG reported the following output: ' . $command_output, array(
'%orig' => $video->uri,
'%preset' => $name,
), WATCHDOG_ERROR);
$this
->change_status($video->vid, VIDEO_RENDERING_FAILED);
return FALSE;
}
$video_info = pathinfo($converted_video_path);
$video->converted = new stdClass();
$video->converted->vid = $video->vid;
$video->converted->filename = $video_info['basename'];
$video->converted->uri = $converted;
$video->converted->filemime = file_get_mimetype($converted);
$video->converted->filesize = filesize($converted);
$video->converted->status = VIDEO_RENDERING_COMPLETE;
$video->converted->preset = $name;
$video->converted->completed = time();
$converted_files[] = $video->converted;
}
db_update('video_files')
->fields(array(
'status' => VIDEO_RENDERING_COMPLETE,
'completed' => time(),
'data' => serialize($converted_files),
))
->condition('vid', $video->converted->vid, '=')
->execute();
watchdog('transcoder', 'Successfully converted %orig to %dest', array(
'%orig' => $video->uri,
'%dest' => $video->converted->uri,
), WATCHDOG_INFO);
return TRUE;
}
public function get_video_info($video) {
static $command_ouput;
if (!empty($command_output)) {
return $command_output;
}
$file = escapeshellarg($video);
$options = '!ffmpeg -i ' . $file;
$command_output = $this
->run_command($options);
return $command_output;
}
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 = preg_match('/([0-9]{1,5})x([0-9]{1,5})/', $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',
'#markup' => '<div id="video_ffmpeg">',
);
$form['video_ffmpeg_nice_enable'] = array(
'#type' => 'checkbox',
'#title' => t('Enable the use of <b>nice</b> when calling the 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['transcoders'] = array(
'#type' => 'fieldset',
'#title' => t('Path to Transcoder Executables'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['transcoders']['video_ffmpeg_path'] = array(
'#type' => 'textfield',
'#title' => t('FFMPEG'),
'#description' => t('Absolute path to ffmpeg executable. This will provide a token of !ffmpeg to preset commands.'),
'#default_value' => variable_get('video_ffmpeg_path', '/usr/bin/ffmpeg'),
);
$form['transcoders']['video_ffmpeg2theora_path'] = array(
'#type' => 'textfield',
'#title' => t('Ffmpeg2Theora'),
'#description' => t('Absolute path to ffmpeg2theora executable. This will provide a token of !ffmpeg2theora to preset commands.'),
'#default_value' => variable_get('video_ffmpeg2theora_path', '/usr/bin/ffmpeg2theora'),
);
$form['transcoders']['video_macoder_path'] = array(
'#type' => 'textfield',
'#title' => t('Mcoder'),
'#description' => t('Absolute path to Mcoder executable. This will provide a token of !macoder to preset commands.'),
'#default_value' => variable_get('video_macoder_path', '/usr/bin/mcoder'),
);
$form['transcoders']['video_handbreke_path'] = array(
'#type' => 'textfield',
'#title' => t('HandBrakeCLI'),
'#description' => t('Absolute path to Handbreke executable. This will provide a token of !handbreke to preset commands.'),
'#default_value' => variable_get('video_handbreke_path', '/usr/bin/HandBrakeCLI'),
);
$form['transcoders']['video_other_path'] = array(
'#type' => 'textfield',
'#title' => t('Other'),
'#description' => t('Absolute path to other transcoder executable. This will provide a token of !other to preset commands.'),
'#default_value' => variable_get('video_other_path', ''),
);
$form['autothumb'] = array(
'#type' => 'fieldset',
'#title' => t('Video Thumbnails'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$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', 'videos/thumbnails'),
);
$form['autothumb']['video_thumbs'] = array(
'#type' => 'textfield',
'#title' => t('Number of thumbnails'),
'#description' => t('Number of thumbnails to extract from video.'),
'#default_value' => variable_get('video_thumbs', 5),
);
$form['autothumb']['video_thumb_save_all'] = array(
'#type' => 'checkbox',
'#title' => t('Save all thumbnails in {file_manged} table'),
'#description' => t('Save all auto created thumbnails to the {file_managed} table. Change file status as PERMANENT'),
'#default_value' => variable_get('video_thumb_save_all', FALSE),
);
$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' => variable_get('video_ffmpeg_thumbnailer_options', '!ffmpeg -i !videofile -an -y -f mjpeg -ss !seek -vframes 1 !thumbfile'),
);
$form['autoconv'] = array(
'#type' => 'fieldset',
'#title' => t('Advanced 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' => variable_get('video_ffmpeg_enable_faststart', 0),
);
$form['autoconv']['video_ffmpeg_faststart_cmd'] = array(
'#type' => 'textfield',
'#title' => t('Path to qt-faststart'),
'#default_value' => variable_get('video_ffmpeg_faststart_cmd', '/usr/bin/qt-faststart'),
);
$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',
'#markup' => '</div>',
);
return $form;
}
public function admin_settings_validate($form, &$form_state) {
return;
}
public function create_job($video, $nid) {
return db_insert('video_files')
->fields(array(
'fid' => $video['fid'],
'nid' => $nid,
'status' => VIDEO_RENDERING_PENDING,
'dimensions' => $video['dimensions'],
))
->execute();
}
public function delete_job($video) {
$video = (object) $video;
if (!($video = $this
->load_job($video->fid))) {
return;
}
$converted = unserialize($video->data);
if (!empty($converted)) {
foreach ($converted as $file) {
if (file_exists(drupal_realpath($file->uri))) {
@drupal_unlink($file->uri);
}
}
}
db_delete('video_files')
->condition('fid', $video->fid)
->execute();
}
public function load_job($fid) {
$job = null;
$job = db_query("SELECT f.*, vf.vid, vf.nid, vf.dimensions, vf.data, vf.status as video_status\n FROM {video_files} vf LEFT JOIN {file_managed} f ON vf.fid = f.fid WHERE f.fid=vf.fid AND f.fid = :fid", array(
':fid' => $fid,
))
->fetch();
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 {file_managed} f ON vf.fid = f.fid
WHERE vf.status = :vstatus AND f.status = :fstatus ORDER BY f.timestamp', 0, $total_videos, array(
':vstatus' => VIDEO_RENDERING_PENDING,
':fstatus' => FILE_STATUS_PERMANENT,
));
foreach ($result as $row) {
$videos[] = $row;
}
return $videos;
}
public function load_completed_job(&$video) {
$file = $this
->load_job($video->fid);
$data = unserialize($file->data);
if (!empty($data)) {
foreach ($data as $value) {
$extension = pathinfo(drupal_realpath($value->uri), PATHINFO_EXTENSION);
$video->files->{$extension}->filename = $value->filename;
$video->files->{$extension}->filepath = $value->uri;
$video->files->{$extension}->filemime = file_get_mimetype($value->uri);
$video->files->{$extension}->url = file_create_url($value->uri);
$video->files->{$extension}->uri = $value->uri;
$video->files->{$extension}->extension = $extension;
$video->player = strtolower($extension);
}
}
else {
return FALSE;
}
}
public function change_status($vid, $status) {
db_update('video_files')
->fields(array(
'status' => $status,
))
->condition('vid', $vid, '=')
->execute();
}
public function dimensions($video) {
$aspect_ratio = _video_aspect_ratio(drupal_realpath($video->uri));
$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;
}
}
}