php-file-layer/VideoUtils.php

181 lines
7.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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;
}
}