El poder de múltiples núcleos para domesticar el códec AV1

imagen



Prólogo



De vez en cuando, me interesan los códecs de video y cuánto más eficientes son en comparación con sus predecesores. En un momento, cuando HEVC salió después de H264, estaba muy interesado en tocarlo, pero mi hardware de esa época dejaba mucho que desear.



Ahora el hardware se ha ajustado, pero HEVC ha estado desactualizado durante mucho tiempo, está ansioso por reemplazarlo con el AV1 abierto, que nos promete hasta un 50% de ahorro en comparación con 1080p H264, pero si la velocidad de codificación de alta calidad en HEVC parece lenta (en comparación con H264), entonces AV1 es su ~ 0.2 fps desmoraliza completamente. Cuando algo se codifica tan lentamente, significa que incluso un simple video de 10 minutos tardará aproximadamente un día en procesarse. Aquellos. solo para ver si los parámetros de codificación son adecuados o si necesita agregar un poco de tasa de bits, tendrá que esperar no solo horas, sino días ...



Y así, un día, mientras admiraba la hermosa puesta de sol (códec H264), pensé: "¿Y si ponemos todo el hardware que tengo en AV1 al mismo tiempo?"



Idea



Intenté codificar AV1 usando mosaicos y multinúcleo, pero la ganancia de rendimiento me pareció no tan efectiva para cada núcleo de procesador agregado, dando aproximadamente un FPS y medio en la configuración más rápida y 0.2 en la más lenta, por lo que se me ocurrió una idea radicalmente diferente.



Después de ver lo que tenemos para hoy en AV1, hice una lista:



  • Codificador libaom-av1 incorporado de ffmpeg
  • Proyecto Rav1e
  • Proyecto SVT-AV1


De todo lo anterior, elegí rav1e. Mostró un rendimiento de un solo subproceso muy bueno y encajó perfectamente en el sistema que se me ocurrió:



  • El codificador cortará el video original en pedazos durante n segundos.
  • Cada una de mis computadoras tendrá un servidor web con un script especial
  • Codificamos en una secuencia, lo que significa que el servidor puede codificar simultáneamente tantas piezas como núcleos de procesador tenga
  • El codificador enviará las piezas a los servidores y descargará los resultados codificados.
  • Cuando todas las piezas estén listas, el codificador las pegará en una y superpondrá el sonido del archivo original.


Implementación



Debo decir de inmediato que la implementación se realiza bajo Windows. En teoría, nada me impide hacer lo mismo con otros sistemas operativos, pero lo hice por lo que tenía.



Así que necesitamos:



  • Servidor web PHP
  • ffmpeg
  • rav1e


1. Primero, necesitamos un servidor web, no describiré qué y cómo lo configuro, para esto hay muchas instrucciones para todos los gustos y colores. Usé Apache + PHP. Es importante que PHP realice una configuración que le permita recibir archivos grandes (por defecto en la configuración 2MB y esto no es suficiente, nuestras piezas pueden ser más grandes). Nada especial sobre complementos, CURL, JSON.



También mencionaré la seguridad, que no existe. Todo lo que hice, lo hice dentro de la red local, por lo que no se realizaron verificaciones ni autorizaciones, y hay muchas oportunidades de daño por parte de intrusos. Por lo tanto, si esto se va a probar en redes no seguras, debe ocuparse de los problemas de seguridad usted mismo.



2. FFmpeg: descargué binarios listos de las compilaciones de Zeranoe



3.rav1e: también puede descargar el binario de las versiones del proyecto rav1e



Script PHP para cada computadora que participará
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 para ejecutar la codificación 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";
	}
}

?>






El archivo para ejecutar el script de codificación está al lado del script. Usted mismo configura la ruta a 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


¿Vamos?



Para la prueba, utilicé la famosa caricatura de Big Bucks Bunny sobre un conejo , de 10 minutos de duración y 150 MB de tamaño.



Planchar



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


Total: 40 hilos



Línea de comando con parámetros



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


resultados



Tiempo de codificación: 55 minutos

Tamaño del video: 75 MB



No hablaré por la calidad, porque la selección de los parámetros de codificación óptimos es la tarea del día de mañana, y hoy perseguía el objetivo de lograr un tiempo de codificación razonable y me parece que funcionó. Tenía miedo de que las piezas pegadas se pegaran mal y en esos momentos hubiera contracciones, pero no, el resultado fue suave, sin tirones.



Por separado, observo que 1080p requiere aproximadamente un gigabyte de RAM por flujo, por lo que debería haber mucha memoria. También tenga en cuenta que hacia el final, la manada corre a la velocidad del ariete más lento y, mientras que Ryzen e i7 hace mucho que terminaron de codificar, el Xeon y el i5 todavía estaban traqueteando sobre sus piezas. Aquellos. un video más largo en general se codificaría a un fps general más alto a expensas de que los núcleos más rápidos hagan más trabajo.



Al ejecutar la conversión en un Ryzen 5 1600 con subprocesos múltiples, el máximo que tenía era de aproximadamente 1,5 fps. Aquí, dado que los últimos 10 minutos de codificación están rematando las últimas piezas con núcleos lentos, podemos decir que resultó en unos 5-6 fps, que no es tan poco para un códec tan avanzado. Eso es todo lo que quería compartir, espero que alguien lo encuentre útil.



All Articles