181 lines
7.2 KiB
PHP
181 lines
7.2 KiB
PHP
<?php
|
||
|
||
class VideoUtils
|
||
{
|
||
public static function getVideoProps($fn)
|
||
{
|
||
if (empty(App::$config['video_converter']))
|
||
{
|
||
throw new Exception('Video converter is not configured on the server');
|
||
}
|
||
$probe = App::$config['video_converter'];
|
||
$cmd = $probe['probe'];
|
||
$cmd = str_replace('$input', escapeshellarg($fn), $cmd);
|
||
$out = shell_exec($cmd);
|
||
$data = [];
|
||
foreach ([ 'format', 'size', 'duration', 'video_format', 'audio_format' ] as $k)
|
||
{
|
||
if (preg_match_all($probe['probe_'.$k], $out, $m, PREG_PATTERN_ORDER))
|
||
{
|
||
unset($m[0]);
|
||
$data[$k] = $m;
|
||
}
|
||
else
|
||
{
|
||
$data[$k] = NULL;
|
||
}
|
||
}
|
||
if ($data['format'] && $data['size'] && $data['duration'])
|
||
{
|
||
$dur = $data['duration'][1][0];
|
||
$colon = explode(':', $dur);
|
||
if (count($colon) > 1)
|
||
{
|
||
$dur = 0;
|
||
for ($mul = 1, $i = 0; $i < count($colon); $i++, $mul *= 60)
|
||
$dur += $colon[count($colon)-1-$i] * $mul;
|
||
}
|
||
// Пустой формат видео означает, что файл ещё нужно обработать (минимум qt-faststart и вытащить тамбик)
|
||
return [
|
||
'format' => 'video',
|
||
'width' => $data['size'][1][0],
|
||
'height' => $data['size'][2][0],
|
||
'props' => [
|
||
'duration' => $dur,
|
||
'video_format' => @$data['video_format'][1],
|
||
'audio_format' => @$data['audio_format'][1],
|
||
],
|
||
];
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
protected static function extractVideoPreview($file, $redirect_output = '')
|
||
{
|
||
$conv = App::$config['video_converter'];
|
||
$ss = $file['props']['duration'] * $conv['preview_moment'];
|
||
$fn = FileHandler::getPath(false, $file);
|
||
$out = FileHandler::getThumbPath(false, $file, 'src');
|
||
FileUtils::mkpath(dirname($out), true);
|
||
$cmd = $conv['extract_frame'];
|
||
$cmd = str_replace([ '$input', '$output', '$position' ], [ escapeshellarg($fn), escapeshellarg($out), ceil($ss) ], $cmd);
|
||
system($cmd . ($redirect_output ? ' &> '.$redirect_output : ''));
|
||
if (!file_exists($out) || !filesize($out))
|
||
{
|
||
throw new Exception('Failed to extract video frame for preview');
|
||
}
|
||
}
|
||
|
||
public static function runConvertJobDaemon($redirect_output = '')
|
||
{
|
||
while (1)
|
||
{
|
||
$file = App::$db->selectRow(File::$table, '*', [ 'format' => 'video' ], [ 'LIMIT' => 1 ]);
|
||
if ($file)
|
||
{
|
||
self::convertVideoAndUpdate($file, $redirect_output);
|
||
}
|
||
App::$db->commitAll();
|
||
sleep(5);
|
||
}
|
||
}
|
||
|
||
public static function convertVideoAndUpdate($file, $redirect_output = '')
|
||
{
|
||
$props = self::convertVideo($file, $redirect_output);
|
||
if (!$props)
|
||
{
|
||
return;
|
||
}
|
||
self::extractVideoPreview($props + $file, $redirect_output);
|
||
$update = $props;
|
||
if (isset($update['props']))
|
||
{
|
||
$update['props'] = json_encode($update['props']);
|
||
}
|
||
App::$db->update(File::$table, $update, [ 'id' => $file['id'] ]);
|
||
if (isset($props['sha1']) && $props['sha1'] != $file['sha1'])
|
||
{
|
||
@unlink(FileHandler::getPath(false, $file));
|
||
}
|
||
elseif (isset($props['format']) && $props['format'] != $file['format'])
|
||
{
|
||
@rename(FileHandler::getPath(false, $file), FileHandler::getPath(false, $props + $file));
|
||
}
|
||
return $props + $file;
|
||
}
|
||
|
||
protected static function convertVideo($file, $redirect_output = '')
|
||
{
|
||
// Краткая сводка:
|
||
// Flash понимает форматы файлов MP4/FLV с видео+аудио FLV+MP3 или H.264+AAC
|
||
// HTML5 кроссбраузерно понимает форматы MP4 с видео H.264 и аудио MP3/AAC, либо WebM с видео VP8 и аудио Vorbis
|
||
// Для MP4 надо делать qt-faststart, если MOOV ATOM не находится в начале видеофайла
|
||
//
|
||
// Итак, если формат WebM+VP8+Vorbis или FLV+FLV+MP3 или FLV+H.264+AAC, видео можно вообще не конвертировать
|
||
// Если формат MP4+H.264+MP3/AAC, надо проверить/сделать qt-faststart
|
||
// Любой другой формат надо перегнать в MP4+H.264+AAC
|
||
if ($file['format'] != 'video')
|
||
{
|
||
return NULL;
|
||
}
|
||
if (empty(App::$config['video_converter']))
|
||
{
|
||
throw new Exception('Video converter is not configured on the server');
|
||
}
|
||
$conv = App::$config['video_converter'];
|
||
$vf = $file['props']['video_format'][0];
|
||
$af = $file['props']['audio_format'][0];
|
||
if ($file['mimetype'] == 'video/x-flv' && ($vf == 'flv1' && $af == 'mp3' || $vf == 'h264' && $af == 'aac'))
|
||
{
|
||
return [
|
||
'format' => 'flv',
|
||
];
|
||
}
|
||
elseif ($file['mimetype'] == 'video/webm' && $vf == 'vp8' && $af == 'vorbis')
|
||
{
|
||
return [
|
||
'format' => 'webm',
|
||
];
|
||
}
|
||
$fn = FileHandler::getPath(false, $file);
|
||
$out = tempnam(sys_get_temp_dir(), 'ffc');
|
||
if ($file['mimetype'] == 'video/mp4' && $vf == 'h264' && ($af == 'mp3' || $af == 'aac'))
|
||
{
|
||
$cmd = $conv['qt_faststart'].' '.escapeshellarg($fn).' '.escapeshellarg($out);
|
||
system($cmd . ($redirect_output ? ' &> '.$redirect_output : ''));
|
||
if (!file_exists($out))
|
||
{
|
||
// Уже было faststart, всё ок
|
||
return [
|
||
'format' => 'mp4',
|
||
];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
$cmd = $conv['convert'];
|
||
$cmd = str_replace([ '$input', '$output' ], [ escapeshellarg($fn), escapeshellarg($out) ], $cmd);
|
||
system($cmd . ($redirect_output ? ' &> '.$redirect_output : ''));
|
||
if (!file_exists($out))
|
||
{
|
||
// Не сконвертировалось, плохо
|
||
throw new Exception('Failed to convert video to MP4');
|
||
}
|
||
}
|
||
// Сконвертированный файл
|
||
$update = FileUtils::getProps(File::ONLY_VIDEO, $this->config['mime_blacklist'], $out, $out);
|
||
$update['format'] = 'mp4';
|
||
$newfn = FileHandler::getPath(false, $update + $file);
|
||
FileUtils::mkpath(dirname($newfn), true);
|
||
if (!@rename($out, $newfn))
|
||
{
|
||
$error = error_get_last();
|
||
throw new Exception($error['message']);
|
||
}
|
||
// FIXME Теоретически может получиться так, что полученный файл будет дубликатом, и запрос свалится
|
||
// (Может ли ffmpeg дважды выдать одинаковый файл? Тогда будет проблемка)
|
||
return $update;
|
||
}
|
||
}
|