プロローグ
時々、私はビデオコーデックスとそれらが前任者と比較してどれほど効率的であるかに興味があります。かつて、H264の後にHEVCが出たとき、私はそれに触れることに非常に興味を持っていましたが、当時の私のハードウェアには多くの要望がありました。
現在、ハードウェアは強化されていますが、HEVCは古く、オープンAV1に置き換えることを熱望しています。これにより、1080p H264と比較して最大50%の節約が約束されますが、HEVCでの高品質エンコーディングの速度が遅いと思われる場合(H264と比較して)、AV1はその〜0.2fpsは完全に士気をくじく。エンコードが非常に遅い場合、単純な10分のビデオでも処理に約1日かかることを意味します。それら。エンコーディングパラメータが適切かどうかを確認するため、またはビットレートを少し追加する必要がある場合は、数時間だけでなく数日も待つ必要があります...
それで、ある日、美しい夕日(H264コーデック)を眺めながら、「AV1にあるすべてのハードウェアを同時に配置したらどうなるだろうか」と思いました。
考え
タイルとマルチコアを使用してAV1をエンコードしようとしましたが、追加されたプロセッサコアごとにパフォーマンスの向上があまり効果的ではなく、最も速い設定で約1.5 FPS、最も遅い設定で0.2となるため、根本的に異なるアイデアが思い浮かびました。
AV1で今日の予定を確認した後、リストを作成しました。
上記のすべてから、私はrav1eを選びました。それは非常に優れたシングルスレッドのパフォーマンスを示し、私が思いついたシステムに完全に適合しました。
- エンコーダーは元のビデオをn秒間細かく切ります
- 私の各コンピューターには、特別なスクリプトを備えたWebサーバーがあります
- 1つのストリームでエンコードします。つまり、サーバーはプロセッサコアと同じ数のピースを同時にエンコードできます。
- エンコーダーはピースをサーバーに送信し、エンコードされた結果をダウンロードして戻します
- すべてのピースの準備ができたら、エンコーダーはそれらを1つに接着し、元のファイルのサウンドをオーバーレイします
実装
実装はWindowsで行われているとすぐに言わなければなりません。理論的には、他のOSでも同じことをするのを妨げるものは何もありませんが、私は自分が持っていたものに対してそれを行いました。
したがって、次のものが必要です。
- PHPWebサーバー
- 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
行きますか?
テストには、長さ10分、サイズ150MBのウサギについての有名なBig BucksBunnyの漫画を使用しました。
鉄
- AMD Ryzen 5 1600(12スレッド)+ 16GB DDR4(Windows 10)
- Intel Core i7 4770(8スレッド)+ 32GB DDR3(Windows 10)
- Intel Core i5 3570(4スレッド)+ 8GB DDR3(Windows 10)
- Intel Xeon 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はストリームごとに約ギガバイトのRAMを必要とするため、大量のメモリが必要になることに注意してください。また、終わりに向かって、群れは最も遅いラムの速度で実行されており、Ryzenとi7がコーディングを終了してから長い間、Xeonとi5はまだそれらのピースをかき回していたことに注意してください。それら。一般に、より長いビデオは、より高速なコアがより多くの作業を行うことを犠牲にして、より高い全体的なfpsでエンコードされます。
マルチスレッドを使用して1つのRyzen5 1600で変換を実行すると、最大は約1.5fpsでした。ここで、エンコードの最後の10分間が遅いコアで最後の部分を終了していることを考えると、約5〜6 fpsであると言えます。これは、このような高度なコーデックではそれほど少なくありません。それが私が共有したかったすべてです、誰かがそれが役に立つと思うかもしれないことを願っています。