- PVSM.RU - https://www.pvsm.ru -
Периодически, я интересуюсь видеокодеками и тем, насколько они становятся эффективнее по сравнению со своими предшественниками. В свое время, когда после H264 вышел HEVC, мне было безумно интересно его пощупать, но мое железо того времени оставляло желать лучшего.
Сейчас же железо подтянулось, но и HEVC давно устарел, ему на смену жаждет придти открытый AV1, обещающий нам до 50% экономии по сравнению с 1080p H264, но если скорость качественного кодирования в HEVC кажется медленноватой (по сравнения с H264), то AV1 со своим ~0.2 fps деморализует полностью. Когда что-то кодируется настолько медленно, то это значит, что даже простой 10 минутный ролик, будет обрабатываться около суток. Т.е. чтобы просто посмотреть подходят ли параметры кодирования или нужно добавить немного битрейта, придется ждать не просто часами, а днями…
И вот, как-то раз, любуясь красивым закатом (кодека H264), я подумал: «А что, если натравить на AV1 все железо которое у меня есть одновременно?»
Я пробовал кодировать AV1 с использование тайлов и многоядерности, но прирост производительности показался мне не таким уж эффективным на каждое добавленное ядро процессора, давая около полтора FPS при самых быстрых настройках и 0.2 при медленных, поэтому в голову пришла кардинально другая идея.
Посмотрев, что у нас есть на сегодня актуального по AV1, я составил список:
Из всего вышеперечисленного я выбрал rav1e. Он показал очень неплохую однопоточную производительность и идеально ложился в систему, которую я придумал:
Сразу скажу, что реализация сделана под Windows. В теории ничего не мешает сделать тоже самое и под другие ОС, но я делал под то, что стояло у меня.
Итак нам нужно:
1. Для начала нам понадобится Web-сервер, я не буду расписывать, что и как я настраивал, для этого есть очень много инструкций на любой вкус и цвет. Я использовал Apache + PHP. Для PHP важно сделать настройку позволяющую ему получать большие файлы (по дефолту в настройках 2Мб и это мало, наши кусочки могут быть больше). Из плагинов ничего особенного, CURL, JSON.
Также упомяну про безопасность, которой нет. Все, что я делал — я делал внутри локальной сети, поэтому никаких проверок и авторизаций не сделано, а возможностей для нанесения вреда злоумышленниками — полно. Поэтому, если это будет тестироваться не в защищенных сетях, вопросами безопасности нужно озаботится самостоятельно.
2. FFmpeg — готовые бинарники я качал с Zeranoe builds [3]
3. rav1e — также можно скачать бинарник из релизов проекта rav1e [1]
В результате если:
Соглашусь, что тут что-то может пойти не так, но только в нештатных режимах, вроде перезагрузки ПК с недоделанной работой, но… черт с ним, сойдет, я договорился со своей совестью.
Скрипт сообщает сколько у него всего потоков и сколько использовано, чтобы кодировщик решал, присылать ему еще кусочки или нет. Также скрипт сообщает о том, какие кусочки сейчас в работе и какие готовы, чтобы кодировщик мог скачать готовые и удалить их с сервера.
encoding.php:
<?php
function getRoot()
{
$root = $_SERVER['DOCUMENT_ROOT'];
if (strlen($root) == 0)
{
$root = dirname(__FILE__)."\..";
}
return $root;
}
function getStoragePath()
{
return getRoot()."\storage";
}
function get_total_cpu_cores()
{
$coresFileName = getRoot()."\cores.txt";
if (file_exists($coresFileName))
{
return intval(file_get_contents($coresFileName));
}
return (int) ((PHP_OS_FAMILY == 'Windows')?(getenv("NUMBER_OF_PROCESSORS")+0):substr_count(file_get_contents("/proc/cpuinfo"),"processor"));
}
function antiHack($str)
{
$strOld = "";
while ($strOld != $str)
{
$strOld = $str;
$str = str_replace("\", "", $str);
$str = str_replace("/", "",$str);
$str = str_replace("|","", $str);
$str = str_replace("..","", $str);
}
return $str;
}
$filesDir = getStoragePath()."\encfiles";
if (!is_dir($filesDir))
{
mkdir($filesDir);
}
$resultDir = $filesDir."\result";
if (!is_dir($resultDir))
{
mkdir($resultDir);
}
$active = glob($filesDir.'\*.cmd');
$all = glob($resultDir.'\*.*');
$info = [
"active" => count($active),
"total" => get_total_cpu_cores(),
"inProgress" => [],
"done" => []
];
foreach ($all as $key)
{
$pi = pathinfo($key);
$commandFile = $pi["filename"].".cmd";
$sourceFile = $pi["filename"];
if (file_exists($filesDir.'\'.$sourceFile))
{
if (file_exists($filesDir.'\'.$commandFile))
{
$info["inProgress"][] = $sourceFile;
}
else
{
$info["done"][] = $sourceFile;
}
}
}
if (isset($_GET["action"]))
{
if ($_GET["action"] == "upload" && isset($_FILES['encfile']) && isset($_POST["params"]))
{
$params = json_decode(hex2bin($_POST["params"]), true);
$fileName = $_FILES['encfile']['name'];
$fileToProcess = $filesDir."\".$fileName;
move_uploaded_file($_FILES['encfile']['tmp_name'], $fileToProcess);
$commandFile = $fileToProcess.".cmd";
$resultFile = $resultDir."\".$fileName.$params["outputExt"];
$command = $params["commandLine"];
$command = str_replace("%SRC%", $fileToProcess, $command);
$command = str_replace("%DST%", $resultFile, $command);
$command .= PHP_EOL.'DEL /Q "'.$commandFile.'"';
file_put_contents($commandFile, $command);
pclose(popen('start "" /B "'.$commandFile.'"', "r"));
}
if ($_GET["action"] == "info")
{
header("Content-Type: application/json");
echo json_encode($info);
die();
}
if ($_GET["action"] == "get")
{
if (isset($_POST["name"]) && isset($_POST["params"]))
{
$params = json_decode(hex2bin($_POST["params"]), true);
$fileName = antiHack($_POST["name"]);
$fileToGet = $filesDir."\".$fileName;
$commandFile = $fileToGet.".cmd";
$resultFile = $resultDir."\".$fileName.$params["outputExt"];
if (file_exists($fileToGet) && !file_exists($commandFile) && file_exists($resultFile))
{
$fp = fopen($resultFile, 'rb');
header("Content-Type: application/octet-stream");
header("Content-Length: ".filesize($resultFile));
fpassthru($fp);
exit;
}
}
}
if ($_GET["action"] == "remove")
{
if (isset($_POST["name"]) && isset($_POST["params"]))
{
$params = json_decode(hex2bin($_POST["params"]), true);
$fileName = antiHack($_POST["name"]);
$fileToGet = $filesDir."\".$fileName;
$commandFile = $fileToGet.".cmd";
$resultFile = $resultDir."\".$fileName.$params["outputExt"];
if (file_exists($fileToGet) && !file_exists($commandFile))
{
if (file_exists($resultFile))
{
unlink($resultFile);
}
unlink($fileToGet);
header("Content-Type: application/json");
echo json_encode([ "result" => true ]);
die();
}
}
header("Content-Type: application/json");
echo json_encode([ "result" => false ]);
die();
}
}
echo "URL Correct";
?>
Тут прописываются все ваши серверы:
$servers = [
"LOCAL" => "http://127.0.0.1:8000/remote/encoding.php",
"SERVER2" => "http://192.168.100.25:8000/remote/encoding.php",
];
encode.php:
<?php
$ffmpeg = '"c:AppsOneDrivecommandsbinffmpegffmpeg.exe"';
$params = [
"commandLine" => '"c:AppsOneDrivecommandsbinffmpegffmpeg" -i "%SRC%" -an -pix_fmt yuv420p -f yuv4mpegpipe - | "c:AppsOneDrivecommandsbinffmpegrav1e" - -s 5 --quantizer 130 -y --output "%DST%"',
"outputExt" => ".ivf"
];
$paramsData = bin2hex(json_encode($params));
$servers = [
"LOCAL" => "http://127.0.0.1:8000/remote/encoding.php",
"SERVER2" => "http://192.168.100.25:8000/remote/encoding.php",
];
if (isset($argc))
{
if ($argc > 1)
{
$fileToEncode = $argv[1];
$timeBegin = time();
$pi = pathinfo($fileToEncode);
$filePartName = $pi["dirname"]."\".$pi["filename"]."_part%04d.mkv";
$fileList = $pi["dirname"]."\".$pi["filename"]."_list.txt";
$joinedFileName = $pi["dirname"]."\".$pi["filename"]."_joined.mkv";
$audioFileName = $pi["dirname"]."\".$pi["filename"]."_audio.opus";
$finalFileName = $pi["dirname"]."\".$pi["filename"]."_AV1.mkv";
exec($ffmpeg.' -i "'.$fileToEncode.'" -c copy -an -segment_time 00:00:10 -reset_timestamps 1 -f segment -y "'.$filePartName.'"');
exec($ffmpeg.' -i "'.$fileToEncode.'" -vn -acodec libopus -ab 128k -y "'.$audioFileName.'"');
$files = glob($pi["dirname"]."\".$pi["filename"]."_part*.mkv");
$sourceParts = $files;
$resultParts = [];
$resultFiles = [];
$inProgress = [];
while (count($files) || count($inProgress))
{
foreach ($servers as $server => $url)
{
if( $curl = curl_init() )
{
curl_setopt($curl, CURLOPT_URL, $url."?action=info");
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$out = curl_exec($curl);
curl_close($curl);
$info = json_decode($out, true);
//var_dump($info);
if (count($files))
{
if (intval($info["active"]) < intval($info["total"]))
{
$fileName = $files[0];
$key = pathinfo($fileName)["basename"];
$inProgress[] = $key;
//echo "Server: ".$url."rn";
echo "Sending part ".$key."[TO ".$server."]...";
if (!in_array($key, $info["done"]) && !in_array($key, $info["inProgress"]))
{
$cFile = curl_file_create($fileName);
$post = ['encfile'=> $cFile, 'params' => $paramsData];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url."?action=upload");
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close ($ch);
}
echo " DONErn";
echo " Total: ".count($sourceParts).", In Progress: ".count($inProgress).", Left: ".count($files)."rn";
$files = array_slice($files, 1);
}
}
if (count($info["done"]))
{
foreach ($info["done"] as $file)
{
if (($key = array_search($file, $inProgress)) !== false)
{
set_time_limit(0);
echo "Receiving part ".$file."... [FROM ".$server."]...";
$resultFile = $pi["dirname"]."\".$file.".result".$params["outputExt"];
$fp = fopen($resultFile, 'w+');
$post = ['name' => $file, 'params' => $paramsData];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url."?action=get");
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
//curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
//fclose($fp);
$resultFiles[] = "file ".$resultFile;
$resultParts[] = $resultFile;
$post = ['name' => $file, 'params' => $paramsData];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url."?action=remove");
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
fclose($fp);
unset($inProgress[$key]);
echo " DONErn";
echo " Total: ".count($sourceParts).", In Progress: ".count($inProgress).", Left: ".count($files)."rn";
}
}
}
}
}
usleep(300000);
}
asort($resultFiles);
file_put_contents($fileList, str_replace("\", "/", implode("rn", $resultFiles)));
exec($ffmpeg.' -safe 0 -f concat -i "'.$fileList.'" -c copy -y "'.$joinedFileName.'"');
exec($ffmpeg.' -i "'.$joinedFileName.'" -i "'.$audioFileName.'" -c copy -y "'.$finalFileName.'"');
unlink($fileList);
unlink($audioFileName);
unlink($joinedFileName);
foreach ($sourceParts as $part)
{
unlink($part);
}
foreach ($resultParts as $part)
{
unlink($part);
}
echo "Total Time: ".(time() - $timeBegin)."srn";
}
}
?>
Файл для запуска скрипта кодирования, лежит рядом со скриптом. Путь к PHP настраиваете сами.
encoding.cmd:
@ECHO OFF
cd /d %~dp0
SET /p FILENAME=Drag'n'Drop file here and Press Enter:
..php7php.exe -c ..php7php_standalone.ini encode.php "%FILENAME%"
PAUSE
Для теста я использовал известный мультик про кролика Big Bucks Bunny [4], длиной 10 минут и размером 150Мб.
Итого: 40 потоков
ffmpeg -i "%SRC%" -an -pix_fmt yuv420p -f yuv4mpegpipe - | rav1e - -s 5 --quantizer 130 -y --output "%DST%
Время кодирования: 55минут
Размер видео: 75мб
За качество говорить не буду, потому что подбор оптимальных параметров кодирования это задача дня заврашнего, а сегодня я преследовал цель добиться вменяемого времени кодирования и мне кажется это получилось. Я опасался, что склеенные кусочки склеятся плохо и будут дерганья в этих моментах, но нет, результат шел ровно, без каких-то рывков.
Отдельно отмечу, что для 1080p требуется около гигабайта оперативной памяти на поток, поэтому памяти должно быть много. Также замечу, что под конец стадо бежит со скоростью самого медленного барана и в то время как Ryzen и i7 уже давно закончили кодирование, Xeon и i5 еще продолжали пыхтеть над своими кусочками. Т.е. более длинное видео в целом кодировалось бы с большим общим fps за счет того, что более быстрые ядра успели бы сделать больше работы.
Запуская конвертацию на одном Ryzen 5 1600 с многопоточностью, максимум что я имел было около 1.5 fps. Здесь же, учитывая, что последние 10 минут кодирования — это добивка последних кусочков медленными ядрами, можно сказать, что получилось около 5-6 fps, что уже не так мало для такого продвинутого кодека. Вот и все, чем я хотел поделиться, надеюсь кому-нибудь это может пригодится.
Автор: CodeName33
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/video/355150
Ссылки в тексте:
[1] rav1e: https://github.com/xiph/rav1e
[2] SVT-AV1: https://github.com/OpenVisualCloud/SVT-AV1
[3] Zeranoe builds: https://ffmpeg.zeranoe.com/builds/
[4] Big Bucks Bunny: https://www.youtube.com/watch?v=S2wAMaM2OYQ
[5] Источник: https://habr.com/ru/post/511512/?utm_source=habrahabr&utm_medium=rss&utm_campaign=511512
Нажмите здесь для печати.