Prolog
Dari waktu ke waktu, saya tertarik pada codec video dan seberapa efisien mereka dibandingkan dengan pendahulunya. Pada suatu waktu, ketika HEVC keluar setelah H264, saya sangat tertarik untuk menyentuhnya, tetapi perangkat keras saya saat itu meninggalkan banyak hal yang diinginkan.
Sekarang perangkat kerasnya telah diperketat, tetapi HEVC telah lama usang, sangat ingin menggantinya dengan AV1 terbuka, yang menjanjikan penghematan hingga 50% dibandingkan dengan 1080p H264, tetapi jika kecepatan pengkodean kualitas tinggi di HEVC tampak lambat (dibandingkan dengan H264), maka AV1 adalah ~ 0,2 fpsnya mendemoralisasi sepenuhnya. Ketika sesuatu dikodekan dengan sangat lambat, itu berarti bahwa video 10 menit yang sederhana akan memakan waktu sekitar satu hari untuk diproses. Itu. hanya untuk melihat apakah parameter pengkodean cocok atau jika Anda perlu menambahkan sedikit bitrate, Anda harus menunggu tidak hanya berjam-jam, tetapi juga berhari-hari ...
Jadi, suatu hari, sambil mengagumi indahnya matahari terbenam (codec H264), saya berpikir: "Bagaimana jika kita meletakkan semua perangkat keras yang saya miliki di AV1 pada saat yang sama?"
Ide
Saya mencoba menyandikan AV1 menggunakan ubin dan multicore, tetapi perolehan kinerja bagi saya tampaknya tidak begitu efektif untuk setiap inti prosesor tambahan, memberikan sekitar satu setengah FPS pada pengaturan tercepat dan 0,2 pada yang paling lambat, sehingga ide yang sangat berbeda muncul di benak saya.
Setelah melihat apa yang kita miliki hari ini di AV1, saya membuat daftar:
Dari semua hal di atas, saya memilih rav1e. Itu menunjukkan kinerja single-threaded yang sangat baik dan sangat cocok dengan sistem yang saya buat:
- Pembuat enkode akan memotong video asli menjadi beberapa bagian selama n detik
- Setiap komputer saya akan memiliki server web dengan skrip khusus
- Kami menyandikan dalam satu aliran, yang berarti bahwa server dapat secara bersamaan menyandikan sebanyak mungkin bagian yang memiliki inti prosesor
- Pembuat enkode akan mengirimkan potongan-potongan tersebut ke server, dan mengunduh kembali hasil yang dikodekan
- Ketika semua bagian sudah siap, encoder akan merekatkannya menjadi satu dan melapisi suara dari file aslinya
Penerapan
Saya harus segera mengatakan bahwa penerapannya dilakukan di bawah Windows. Secara teori, tidak ada yang mencegah saya melakukan hal yang sama untuk OS lain, tetapi saya melakukannya untuk apa yang saya miliki.
Jadi kami membutuhkan:
- Server web PHP
- ffmpeg.dll
- rav1e
1. Pertama, kita membutuhkan server Web, saya tidak akan menjelaskan apa dan bagaimana saya mengatur, untuk ini ada banyak instruksi untuk setiap selera dan warna. Saya menggunakan Apache + PHP. Penting bagi PHP untuk membuat pengaturan yang memungkinkan untuk menerima file besar (secara default di pengaturan 2MB dan ini tidak cukup, potongan kami mungkin lebih besar). Tidak ada yang istimewa tentang plugin, CURL, JSON.
Saya juga akan menyebutkan keamanan, yang tidak ada. Semua yang saya lakukan - saya lakukan di dalam jaringan lokal, jadi tidak ada pemeriksaan dan otorisasi yang dilakukan, dan ada banyak peluang untuk diserang oleh penyusup. Oleh karena itu, jika ini akan diuji di jaringan yang tidak diamankan, masalah keamanan perlu ditangani sendiri.
2. FFmpeg - Saya mendownload binari siap pakai dari Zeranoe build
3.rav1e - Anda juga dapat mengunduh biner dari rilis proyek rav1e
Script PHP untuk setiap komputer yang akan berpartisipasi
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";
?>
Skrip lokal untuk menjalankan encode.php encoding
. : , . :
:
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";
}
}
?>
File untuk menjalankan skrip pengkodean berada di sebelah skrip. Anda mengonfigurasi sendiri jalur ke 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
Pergilah?
Untuk pengujian, saya menggunakan kartun Kelinci Besar yang terkenal tentang kelinci , berdurasi 10 menit dan berukuran 150MB.
Besi
- AMD Ryzen 5 1600 (12 Thread) + 16GB DDR4 (Windows 10)
- Intel Core i7 4770 (8 utas) + 32GB DDR3 (Windows 10)
- Intel Core i5 3570 (4 utas) + 8GB DDR3 (Windows 10)
- Intel Xeon E5-2650 V2 (16 utas) + 32GB DDR3 (Windows 10)
Total: 40 utas
Baris perintah dengan parameter
ffmpeg -i "%SRC%" -an -pix_fmt yuv420p -f yuv4mpegpipe - | rav1e - -s 5 --quantizer 130 -y --output "%DST%
hasil
Waktu pengkodean: 55 menit
Ukuran video: 75 MB
Saya tidak akan berbicara tentang kualitas, karena pemilihan parameter pengkodean yang optimal adalah tugas hari sebelumnya, dan hari ini saya mengejar tujuan untuk mencapai waktu pengkodean yang wajar dan menurut saya itu berhasil. Saya takut potongan yang direkatkan akan menempel dengan buruk dan akan ada kedutan pada saat-saat ini, tetapi tidak, hasilnya berjalan mulus, tanpa sentakan.
Secara terpisah, saya perhatikan bahwa 1080p membutuhkan sekitar satu gigabyte RAM per streaming, jadi harus ada banyak memori. Juga perhatikan bahwa menjelang akhir, kawanan berjalan pada kecepatan ram paling lambat, dan sementara Ryzen dan i7 sudah lama menyelesaikan pengkodean, Xeon dan i5 masih membengkak. Itu. video yang lebih panjang secara umum akan dikodekan pada fps keseluruhan yang lebih tinggi dengan mengorbankan inti yang lebih cepat melakukan lebih banyak pekerjaan.
Menjalankan konversi pada satu Ryzen 5 1600 dengan multithreading, maksimum yang saya miliki adalah sekitar 1,5 fps. Di sini, mengingat bahwa 10 menit terakhir pengkodean menyelesaikan bagian terakhir dengan inti yang lambat, kita dapat mengatakan bahwa ternyata sekitar 5-6 fps, yang tidak terlalu sedikit untuk codec tingkat lanjut. Itu saja yang ingin saya bagikan, saya harap seseorang mungkin menganggapnya berguna.