O poder de vários núcleos para domar o codec AV1

imagem



Prólogo



De vez em quando, estou interessado em codecs de vídeo e em como eles são mais eficientes em comparação com seus antecessores. Certa vez, quando o HEVC foi lançado após o H264, eu estava extremamente interessado em tocá-lo, mas meu hardware da época deixava muito a desejar.



Agora o hardware ficou mais rígido, mas HEVC está desatualizado há muito tempo, ele está ansioso para substituí-lo pelo AV1 aberto, o que nos promete economia de até 50% em comparação com 1080p H264, mas se a velocidade de codificação de alta qualidade em HEVC parece lenta (em comparação com H264), então AV1 é seu ~ 0.2 fps desmoraliza completamente. Quando algo é codificado tão lentamente, significa que mesmo um vídeo simples de 10 minutos levará cerca de um dia para ser processado. Essa. só para ver se os parâmetros de codificação são adequados ou se você precisa adicionar um pouco de taxa de bits, você terá que esperar não apenas horas, mas dias ...



E assim, uma vez, admirando o lindo pôr do sol (codec H264), pensei: "E se colocarmos todo o hardware que tenho no AV1 ao mesmo tempo?"



Idéia



Tentei codificar AV1 usando blocos e multicore, mas o ganho de desempenho não me pareceu tão eficaz para cada núcleo de processador adicionado, dando cerca de um FPS e meio nas configurações mais rápidas e 0,2 no mais lento, então uma ideia radicalmente diferente veio à minha mente.



Depois de ver o que temos para hoje no AV1, fiz uma lista:



  • Codificador libaom-av1 integrado do Ffmpeg
  • Projeto Rav1e
  • Projeto SVT-AV1


De todas as opções acima, eu escolhi Rav1e. Ele mostrou um desempenho de thread único muito bom e se encaixou perfeitamente no sistema que criei:



  • O codificador cortará o vídeo original em pedaços por n segundos
  • Cada um dos meus computadores terá um servidor web com um script especial
  • Codificamos em um fluxo, o que significa que o servidor pode codificar simultaneamente tantas peças quanto tiver núcleos de processador
  • O codificador enviará as peças aos servidores e fará o download dos resultados codificados de volta
  • Quando todas as peças estiverem prontas, o codificador irá colá-las em uma e sobrepor o som do arquivo original


Implementação



Devo dizer desde já que a implementação é feita em Windows. Em teoria, nada me impede de fazer a mesma coisa para outros SOs, mas fiz pelo que tinha.



Então, precisamos:



  • Servidor web PHP
  • ffmpeg
  • Rav1e


1. Primeiro, precisamos de um servidor Web, não vou descrever o que e como configurei, para isso existem muitas instruções para cada gosto e cor. Usei Apache + PHP. É importante que o PHP faça uma configuração que lhe permita receber arquivos grandes (por padrão nas configurações 2MB e isso não é suficiente, nossas peças podem ser maiores). Nada de especial sobre plug-ins, CURL, JSON.



Mencionarei também a segurança, que não existe. Tudo o que eu fiz - eu fiz dentro da rede local, então nenhuma verificação e autorização foi feita e há muitas oportunidades para invasores prejudicarem. Portanto, se isso for testado em redes não protegidas, os problemas de segurança precisam ser resolvidos.



2. FFmpeg - baixei binários prontos das compilações do Zeranoe



3.rav1e - você também pode baixar o binário das versões do projeto rav1e



Script PHP para cada computador 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 executar a codificação 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";
	}
}

?>






O arquivo para executar o script de codificação está próximo ao script. Você mesmo configura o caminho para o 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


Ir?



Para o teste, usei o famoso desenho animado Big Bucks Bunny sobre um coelho , com 10 minutos de duração e 150 MB de tamanho.



Ferro



  • 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)


Total: 40 tópicos



Linha de comando com parâmetros



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


resultados



Tempo de codificação: 55 minutos

Tamanho do vídeo: 75 MB



Não falarei pela qualidade, porque a seleção dos parâmetros de codificação ideais é uma tarefa do dia anterior, e hoje eu estava perseguindo o objetivo de conseguir um tempo de codificação razoável e me parece que deu certo. Tive medo de que as peças coladas grudassem mal e houvesse espasmos nesses momentos, mas não, o resultado saiu bem, sem solavancos.



Separadamente, observo que 1080p requer cerca de um gigabyte de RAM por stream, então deve haver muita memória. Observe também que, no final, o rebanho está correndo na velocidade do carneiro mais lento e, embora o Ryzen e o i7 já tenham terminado a codificação, o Xeon e o i5 ainda estavam engolindo suas peças. Essa. um vídeo mais longo em geral seria codificado em fps geral mais alto às custas de núcleos mais rápidos realizando mais trabalho.



Executando a conversão em um Ryzen 5 1600 com multithreading, o máximo que tive foi cerca de 1,5 fps. Aqui, dado que os últimos 10 minutos de codificação estão terminando as últimas partes com núcleos lentos, podemos dizer que resultou em cerca de 5-6 fps, o que não é tão pouco para um codec tão avançado. Isso é tudo que eu queria compartilhar, espero que alguém ache útil.



All Articles