La puissance de plusieurs cœurs pour apprivoiser le codec AV1

image



Prologue



De temps en temps, je m'intéresse aux codecs vidéo et à leur efficacité par rapport à leurs prédécesseurs. À un moment donné, lorsque HEVC est sorti après H264, j'étais extrêmement intéressé à le toucher, mais mon matériel de l'époque laissait beaucoup à désirer.



Maintenant, le matériel s'est resserré, mais HEVC est obsolète depuis longtemps, il est impatient de le remplacer par l'AV1 ouvert, ce qui nous promet jusqu'à 50% d'économies par rapport au 1080p H264, mais si la vitesse d'encodage de haute qualité en HEVC semble lente (par rapport à H264), alors AV1 est son ~ 0,2 fps démoralise complètement. Lorsque quelque chose est encodé si lentement, cela signifie que même une simple vidéo de 10 minutes prendra environ un jour pour être traitée. Ceux. juste pour voir si les paramètres d'encodage conviennent ou si vous avez besoin d'ajouter un peu de bitrate, vous devrez attendre non seulement des heures, mais des jours ...



Et donc, une fois, en admirant le magnifique coucher de soleil (codec H264), j'ai pensé: "Et si on mettait tout le matériel que j'ai sur AV1 en même temps?"



Idée



J'ai essayé d'encoder AV1 en utilisant des tuiles et des multicœurs, mais le gain de performance ne me semblait pas si efficace pour chaque cœur de processeur ajouté, donnant environ un FPS et demi aux réglages les plus rapides et 0,2 au plus lent, donc une idée radicalement différente m'est venue à l'esprit.



Après avoir regardé ce que nous avons pour aujourd'hui sur AV1, j'ai fait une liste:



  • Encodeur libaom-av1 intégré à Ffmpeg
  • Projet Rav1e
  • Projet SVT-AV1


De tout ce qui précède, j'ai choisi rav1e. Il a montré de très bonnes performances à un seul thread et s'intègre parfaitement dans le système que j'ai proposé:



  • L'encodeur coupera la vidéo originale en morceaux pendant n secondes
  • Chacun de mes ordinateurs aura un serveur Web avec un script spécial
  • Nous encodons dans un flux, ce qui signifie que le serveur peut encoder simultanément autant de pièces qu'il a de cœurs de processeur
  • L'encodeur enverra les pièces aux serveurs et téléchargera les résultats encodés
  • Lorsque tous les morceaux sont prêts, l'encodeur les collera en un et superposera le son du fichier d'origine


la mise en oeuvre



Je dois dire tout de suite que l'implémentation se fait sous Windows. En théorie, rien ne m'empêche de faire la même chose pour d'autres OS, mais je l'ai fait pour ce que j'avais.



Donc nous avons besoin:



  • Serveur Web PHP
  • ffmpeg
  • rav1e


1. Premièrement, nous avons besoin d'un serveur Web, je ne décrirai pas ce que j'ai configuré et comment, pour cela il y a beaucoup d'instructions pour chaque goût et couleur. J'ai utilisé Apache + PHP. Il est important pour PHP de faire un paramétrage qui lui permette de recevoir des fichiers volumineux (par défaut dans les paramètres 2 Mo et ce n'est pas suffisant, nos pièces peuvent être plus volumineuses). Rien de spécial sur les plugins, CURL, JSON.



Je mentionnerai également la sécurité, qui n’existe pas. Tout ce que j'ai fait - je l'ai fait à l'intérieur du réseau local, donc aucune vérification ni autorisation n'a été effectuée, et il existe de nombreuses possibilités de préjudice de la part d'intrus. Par conséquent, si cela doit être testé sur des réseaux non sécurisés, vous devez vous occuper des problèmes de sécurité vous-même.



2. FFmpeg - J'ai téléchargé des binaires prêts à partir des versions de Zeranoe



3.rav1e - vous pouvez également télécharger le binaire à partir des versions du projet rav1e



Script PHP pour chaque ordinateur qui participera
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";
?>




Script local pour exécuter l'encodage 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";
	}
}

?>






Le fichier pour exécuter le script de codage se trouve à côté du script. Vous configurez vous-même le chemin vers 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


Aller?



Pour le test, j'ai utilisé le célèbre dessin animé Big Bucks Bunny sur un lapin , d'une durée de 10 minutes et d'une taille de 150 Mo.



Le fer



  • AMD Ryzen 5 1600 (12 fils) + 16 Go de mémoire DDR4 (Windows 10)
  • Intel Core i7 4770 (8 threads) + 32 Go de DDR3 (Windows 10)
  • Intel Core i5 3570 (4 threads) + 8 Go de DDR3 (Windows 10)
  • Intel Xeon E5-2650 V2 (16 threads) + 32 Go de mémoire DDR3 (Windows 10)


Total: 40 fils



Ligne de commande avec paramètres



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


résultats



Temps d'encodage: 55 minutes

Taille de la vidéo : 75 Mo



Je ne parlerai pas de la qualité, car la sélection des paramètres d'encodage optimaux est une tâche de la veille, et aujourd'hui je poursuivais l'objectif d'atteindre un temps d'encodage raisonnable et il me semble que cela a fonctionné. J'avais peur que les pièces collées se collent mal et qu'il y ait des contractions à ces moments-là, mais non, le résultat s'est déroulé sans heurts, sans aucune secousse.



Par ailleurs, je note que 1080p nécessite environ un gigaoctet de RAM par flux, il devrait donc y avoir beaucoup de mémoire. Notez également que vers la fin, le troupeau tourne à la vitesse du bélier le plus lent, et alors que le Ryzen et le i7 avaient fini de coder depuis longtemps, le Xeon et l'i5 soufflaient toujours sur leurs morceaux. Ceux. une vidéo plus longue en général serait encodée à un fps global plus élevé au détriment des cœurs plus rapides faisant plus de travail.



En exécutant la conversion sur un Ryzen 5 1600 avec multithreading, le maximum que j'avais était d'environ 1,5 fps. Ici, étant donné que les 10 dernières minutes d'encodage terminent les dernières pièces avec des cœurs lents, nous pouvons dire qu'il s'est avéré environ 5-6 fps, ce qui n'est pas si peu pour un codec aussi avancé. C'est tout ce que je voulais partager, j'espère que quelqu'un pourrait le trouver utile.



All Articles