Die Fähigkeit mehrerer Kerne, den AV1-Codec zu zähmen

Bild



Prolog



Von Zeit zu Zeit interessiere ich mich für Video-Codecs und wie viel effizienter sie im Vergleich zu ihren Vorgängern sind. Zu einer Zeit, als HEVC nach H264 herauskam, war ich sehr daran interessiert, es zu berühren, aber meine damalige Hardware ließ zu wünschen übrig.



Jetzt hat sich die Hardware verschärft, aber HEVC ist längst veraltet. Es ist bestrebt, sie durch die offene AV1 zu ersetzen, die uns bis zu 50% Einsparungen im Vergleich zu 1080p H264 verspricht. Wenn die Geschwindigkeit der hochwertigen Codierung in HEVC jedoch langsam erscheint (im Vergleich zu H264), ist AV1 dies seine ~ 0,2 fps demoralisieren vollständig. Wenn etwas so langsam codiert wird, bedeutet dies, dass selbst ein einfaches 10-minütiges Video etwa einen Tag benötigt, um verarbeitet zu werden. Jene. Nur um zu sehen, ob die Codierungsparameter geeignet sind oder ob Sie eine kleine Bitrate hinzufügen müssen, müssen Sie nicht nur Stunden, sondern auch Tage warten ...



Als ich eines Tages den wunderschönen Sonnenuntergang (H264-Codec) bewunderte, dachte ich: "Was ist, wenn wir die gesamte Hardware, die ich auf AV1 habe, gleichzeitig installieren?"



Idee



Ich habe versucht, AV1 mit Kacheln und Multicore zu codieren, aber der Leistungsgewinn schien mir nicht für jeden hinzugefügten Prozessorkern so effektiv zu sein, da er bei den schnellsten Einstellungen etwa eineinhalb FPS und bei den langsamsten 0,2 FPS ergab, sodass mir eine radikal andere Idee einfiel.



Nachdem ich mir angesehen habe, was wir heute auf AV1 haben, habe ich eine Liste erstellt:



  • Der integrierte libaom-av1-Encoder von Ffmpeg
  • Rav1e Projekt
  • SVT-AV1- Projekt


Aus all dem habe ich mich für rav1e entschieden. Es zeigte eine sehr gute Single-Threaded-Leistung und passte perfekt in das System, das ich mir ausgedacht hatte:



  • Der Encoder schneidet das Originalvideo für n Sekunden in Stücke
  • Jeder meiner Computer verfügt über einen Webserver mit einem speziellen Skript
  • Wir codieren in einem Stream, was bedeutet, dass der Server gleichzeitig so viele Teile codieren kann, wie er Prozessorkerne hat
  • Der Encoder sendet die Teile an die Server und lädt die codierten Ergebnisse zurück
  • Wenn alle Teile fertig sind, klebt der Encoder sie zu einem zusammen und überlagert den Ton aus der Originaldatei


Implementierung



Ich muss sofort sagen, dass die Implementierung unter Windows erfolgt. Theoretisch hindert mich nichts daran, dasselbe für andere Betriebssysteme zu tun, aber ich habe es für das getan, was ich hatte.



Also brauchen wir:



  • PHP-Webserver
  • ffmpeg
  • rav1e


1. Zunächst benötigen wir einen Webserver. Ich werde nicht beschreiben, was und wie ich eingerichtet habe. Dafür gibt es viele Anweisungen für jeden Geschmack und jede Farbe. Ich habe Apache + PHP verwendet. Für PHP ist es wichtig, eine Einstellung vorzunehmen, die es ihm ermöglicht, große Dateien zu empfangen (standardmäßig in den Einstellungen 2 MB und dies reicht nicht aus, unsere Teile sind möglicherweise größer). Nichts Besonderes an Plugins, CURL, JSON.



Ich werde auch die Sicherheit erwähnen, die es nicht gibt. Alles, was ich getan habe - ich habe es innerhalb des lokalen Netzwerks getan, daher wurden keine Überprüfungen und Berechtigungen durchgeführt, und es gibt viele Möglichkeiten für Eindringlinge, Schaden zuzufügen. Wenn dies in nicht gesicherten Netzwerken getestet werden soll, müssen Sicherheitsprobleme daher selbst behoben werden.



2. FFmpeg - Ich habe fertige Binärdateien von Zeranoe-Builds heruntergeladen



3.rav1e - Sie können die Binärdatei auch aus den Rav1e- Projektversionen herunterladen



PHP-Skript für jeden Computer, der teilnehmen wird
encoding.php, http: // HOST/remote/encoding.php

:



  1. ,
  2. CMD CMD
  3. CMD


:



  1. , CMD —
  2. , 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";
?>




Lokales Skript zum Ausführen der encode.php-Codierung
. : , . :



  • 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";
	}
}

?>






Die Datei zum Ausführen des Codierungsskripts befindet sich neben dem Skript. Sie konfigurieren den Pfad zu PHP selbst.

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


Gehen?



Für den Test habe ich den berühmten Big Bucks Bunny- Cartoon über ein Kaninchen verwendet , das 10 Minuten lang und 150 MB groß ist.



Eisen



  • AMD Ryzen 5 1600 (12 Threads) + 16 GB DDR4 (Windows 10)
  • Intel Core i7 4770 (8 Threads) + 32 GB DDR3 (Windows 10)
  • Intel Core i5 3570 (4 Threads) + 8 GB DDR3 (Windows 10)
  • Intel Xeon E5-2650 V2 (16 Threads) + 32 GB DDR3 (Windows 10)


Gesamt: 40 Fäden



Befehlszeile mit Parametern



ffmpeg -i "%SRC%" -an -pix_fmt yuv420p -f yuv4mpegpipe - | rav1e - -s 5 --quantizer 130  -y --output "%DST%


Ergebnisse



Kodierungszeit: 55 Minuten

Videogröße: 75 MB



Ich werde nicht für die Qualität sprechen, da die Auswahl der optimalen Kodierungsparameter eine Aufgabe des Vortages ist und ich heute das Ziel verfolgt habe, eine angemessene Kodierungszeit zu erreichen, und es scheint mir, dass es geklappt hat. Ich hatte Angst, dass die geklebten Teile schlecht zusammenkleben und in diesen Momenten zucken würden, aber nein, das Ergebnis verlief reibungslos und ohne Rucke.



Unabhängig davon stelle ich fest, dass 1080p ungefähr ein Gigabyte RAM pro Stream benötigt, daher sollte viel Speicher vorhanden sein. Beachten Sie auch, dass die Herde gegen Ende mit der Geschwindigkeit des langsamsten Widder läuft und während Ryzen und i7 die Codierung längst abgeschlossen hatten, tuckerten Xeon und i5 immer noch über ihre Stücke. Jene. Ein längeres Video würde im Allgemeinen mit höheren Gesamt-fps auf Kosten der schnelleren Kerne codiert, die mehr Arbeit leisten.



Bei der Konvertierung auf einem Ryzen 5 1600 mit Multithreading hatte ich maximal 1,5 fps. Angesichts der Tatsache, dass die letzten 10 Minuten der Codierung die letzten Teile mit langsamen Kernen abschließen, können wir sagen, dass es sich um 5-6 fps handelte, was für einen so fortgeschrittenen Codec nicht so wenig ist. Das ist alles, was ich teilen wollte. Ich hoffe, jemand findet es nützlich.



All Articles