序幕
我不时地对视频编解码器感兴趣,以及它们比以前的编解码器效率更高。曾经有一次,当HEVC在H264之后问世时,我对触摸它非常感兴趣,但是当时的硬件仍然有很多不足之处。
现在硬件已经变紧,但HEVC早已过时,它渴望用开放的AV1代替它,与1080p H264相比,它可以节省多达50%的费用,但是如果HEVC中的高质量编码速度似乎很慢(与H264相比),则AV1是它的〜0.2 fps完全使人丧气。当某些东西编码得很慢时,这意味着即使是一个简单的10分钟视频也将需要大约一天的时间来处理。那些。只是为了看看编码参数是否合适,或者是否需要增加一点比特率,您不仅要等待数小时,还要等几天。
因此,有一天,在欣赏美丽的日落(H264编解码器)的同时,我想:“如果我们将我拥有的所有硬件同时放在AV1上怎么办?”
理念
我曾尝试使用图块和多核对AV1进行编码,但对我来说,每个增加的处理器内核的性能提升似乎都不那么有效,在最快的设置下提供大约一半的FPS,在最慢的设置下提供0.2的FPS,所以我想到了一个截然不同的想法。
看完今天关于AV1的内容之后,我列出了一个清单:
从以上所有内容中,我选择了rav1e。它显示出非常好的单线程性能,非常适合我想出的系统:
- 编码器将原始视频切成小块n秒
- 我的每台计算机都会有一个带有特殊脚本的Web服务器
- 我们在一个流中进行编码,这意味着服务器可以同时对具有处理器核心的多个部分进行编码
- 编码器会将片段发送到服务器,然后将编码结果下载回去
- 当所有片段准备就绪时,编码器会将它们粘在一起并覆盖原始文件中的声音
实作
我必须马上说实现是在Windows下进行的。从理论上讲,没有什么可以阻止我在其他操作系统上做同样的事情,但是我是根据自己的能力来做的。
因此,我们需要:
- PHP Web服务器
- ffmpeg
- rav1e
1.首先,我们需要一个Web服务器,我将不介绍设置的方式和方式,为此,对于每种口味和颜色都有很多说明。我使用Apache + PHP。对于PHP来说,进行允许接收大文件的设置非常重要(默认情况下,设置为2MB,这还不够,我们的文件可能更大)。插件,CURL,JSON没什么特别的。
我还将提到安全性,这是不存在的。我所做的一切-我在本地网络中进行的工作,因此没有进行检查和授权,并且入侵者有很多伤害的机会。因此,如果要在不安全的网络中进行测试,则必须自己解决安全问题。
2. FFmpeg-我从Zeranoe版本下载了现成的二进制文件
3.rav1e-您还可以从rav1e项目发行版中下载二进制文件
每台将参与的计算机的PHP脚本
encoding.php, http: // HOST/remote/encoding.php
:
:
, - , , , … , , .
, , . , , .
encoding.php:
:
- ,
- CMD CMD
- CMD
:
- , CMD —
- , CMD —
, - , , , … , , .
, , . , , .
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";
?>
本地脚本运行encode.php编码
. : , . :
:
encode.php:
- c:\Apps\OneDrive\commands\bin\ffmpeg\ffmpeg.exe — Zeranoe builds
- c:\Apps\OneDrive\commands\bin\ffmpeg\rav1e.exe — rav1e
:
$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:\Apps\OneDrive\commands\bin\ffmpeg\ffmpeg.exe"';
$params = [
"commandLine" => '"c:\Apps\OneDrive\commands\bin\ffmpeg\ffmpeg" -i "%SRC%" -an -pix_fmt yuv420p -f yuv4mpegpipe - | "c:\Apps\OneDrive\commands\bin\ffmpeg\rav1e" - -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."\r\n";
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 " DONE\r\n";
echo " Total: ".count($sourceParts).", In Progress: ".count($inProgress).", Left: ".count($files)."\r\n";
$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 " DONE\r\n";
echo " Total: ".count($sourceParts).", In Progress: ".count($inProgress).", Left: ".count($files)."\r\n";
}
}
}
}
}
usleep(300000);
}
asort($resultFiles);
file_put_contents($fileList, str_replace("\\", "/", implode("\r\n", $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)."s\r\n";
}
}
?>
运行编码脚本的文件在脚本旁边。您可以自行配置PHP的路径。
encoding.cmd:
@ECHO OFF
cd /d %~dp0
SET /p FILENAME=Drag'n'Drop file here and Press Enter:
..\php7\php.exe -c ..\php7\php_standalone.ini encode.php "%FILENAME%"
PAUSE
走?
为了进行测试,我使用了著名的Big Bucks Bunny卡通动画,该动画大约长10分钟,大小为150MB。
铁
- AMD锐龙5 1600(12线程)+ 16GB DDR4(Windows 10)
- 英特尔酷睿i7 4770(8线程)+ 32GB DDR3(Windows 10)
- 英特尔酷睿i5 3570(4线程)+ 8GB DDR3(Windows 10)
- 英特尔至强E5-2650 V2(16线程)+ 32GB DDR3(Windows 10)
总数:40个线程
带有参数的命令行
ffmpeg -i "%SRC%" -an -pix_fmt yuv420p -f yuv4mpegpipe - | rav1e - -s 5 --quantizer 130 -y --output "%DST%
结果
编码时间:55分钟
视频大小:75 MB
我不会说质量,因为选择最佳编码参数是前一天的任务,而今天我追求的是实现合理编码时间的目标,对我来说似乎已经解决了。我担心粘在一起的碎片会严重粘在一起,在这些时刻会抽搐,但是不,结果进行得很顺利,没有任何抽搐。
另外,我注意到1080p的每个流需要大约1 GB的RAM,因此应该有很多内存。另请注意,直到最后,牧群都以最慢的内存速度运行,而Ryzen和i7早已完成编码,而Xeon和i5仍在争分夺秒。那些。通常,较长的视频将以较高的总fps进行编码,但会耗费更快的核心来完成更多工作。
在具有多线程功能的一台Ryzen 5 1600上运行转换,我的最高速度约为1.5 fps。在这里,考虑到最后10分钟的编码已经完成了慢速内核的最后部分,我们可以说结果约为5-6 fps,对于这样的高级编解码器来说,这并不是很多。这就是我要分享的所有内容,希望有人会觉得它有用。