php-file-layer/VideoUtils.php

181 lines
7.2 KiB
PHP
Raw Normal View History

<?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');
}
}
// Сконвертированный файл
2018-09-10 02:33:47 +03:00
$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;
}
}