قوة النوى المتعددة لترويض برنامج ترميز AV1

صورة



مقدمة



من وقت لآخر ، أنا مهتم ببرامج ترميز الفيديو ومدى فعاليتها مقارنة بسابقاتها. في وقت من الأوقات ، عندما ظهر HEVC بعد H264 ، كنت مهتمًا جدًا بلمسه ، لكن أجهزتي في ذلك الوقت تركت الكثير مما هو مرغوب فيه.



الآن تم تشديد الأجهزة ، لكن HEVC عفا عليها الزمن منذ فترة طويلة ، فهي حريصة على استبدالها بـ AV1 المفتوح ، والذي يعدنا بتوفير يصل إلى 50٪ مقارنة بـ 1080p H264 ، ولكن إذا كانت سرعة التشفير عالي الجودة في HEVC تبدو بطيئة (مقارنة بـ H264) ، فإن AV1 تكون بطيئة ~ 0.2 إطارًا في الثانية يحبط معنوياته تمامًا. عندما يتم ترميز شيء ما ببطء شديد ، فهذا يعني أنه حتى مقطع فيديو بسيط مدته 10 دقائق سيستغرق يومًا تقريبًا حتى تتم معالجته. أولئك. فقط لمعرفة ما إذا كانت معلمات التشفير مناسبة أو إذا كنت بحاجة إلى إضافة القليل من معدل البت ، فسيتعين عليك الانتظار ليس فقط لساعات ، ولكن لأيام ...



وهكذا ، بمجرد الإعجاب بغروب الشمس الجميل (برنامج الترميز H264) ، فكرت: "ماذا لو وضعنا جميع الأجهزة التي لدي على AV1 في نفس الوقت؟"



فكرة



حاولت ترميز AV1 باستخدام البلاط ومتعدد النوى ، ولكن بدا لي أن مكاسب الأداء ليست فعالة جدًا لكل نواة معالج مضافة ، حيث أعطت حوالي واحد ونصف إطارًا في الثانية في أسرع الإعدادات و 0.2 في أبطأ ، لذلك خطرت لي فكرة مختلفة جذريًا.



بعد الاطلاع على ما لدينا اليوم على AV1 ، قمت بعمل قائمة:



  • مشفر ffmpeg المدمج libaom-av1
  • مشروع Rav1e
  • مشروع SVT-AV1


من كل ما سبق ، اخترت rav1e. لقد أظهر أداءً أحاديًا جيدًا للغاية ويتناسب تمامًا مع النظام الذي توصلت إليه:



  • سيقطع برنامج التشفير الفيديو الأصلي إلى أجزاء لمدة n ثانية
  • سيكون لكل جهاز من أجهزة الكمبيوتر الخاصة بي خادم ويب بنص خاص
  • نقوم بالتشفير في دفق واحد ، مما يعني أن الخادم يمكنه في نفس الوقت تشفير العديد من القطع التي تحتوي على نوى المعالج
  • سيرسل المشفر القطع إلى الخوادم ، ويعيد تنزيل النتائج المشفرة
  • عندما تكون جميع القطع جاهزة ، سيقوم برنامج التشفير بلصقها في قطعة واحدة وتراكب الصوت من الملف الأصلي


التنفيذ



يجب أن أقول على الفور أن التنفيذ يتم تحت Windows. من الناحية النظرية ، لا شيء يمنعني من فعل الشيء نفسه مع أنظمة تشغيل أخرى ، لكنني فعلت ذلك من أجل ما كان لدي.



لذلك نحن بحاجة:



  • خادم الويب PHP
  • ffmpeg
  • rav1e


1. بادئ ذي بدء ، نحن بحاجة إلى خادم ويب ، ولن أصف ماذا وكيف أقوم بإعداده ، ولهذا يوجد الكثير من التعليمات لكل ذوق ولون. لقد استخدمت Apache + PHP. من المهم أن تقوم PHP بعمل إعداد يسمح لها باستقبال الملفات الكبيرة (افتراضيًا في الإعدادات 2 ميجابايت وهذا لا يكفي ، قد تكون قطعنا أكبر). لا يوجد شيء مميز حول المكونات الإضافية ، CURL ، JSON.



كما سأذكر الأمن غير الموجود. كل ما فعلته - فعلته داخل الشبكة المحلية ، لذلك لم يتم إجراء عمليات فحص وتفويضات ، وهناك الكثير من فرص الأذى من قبل المتسللين. لذلك ، إذا كان سيتم اختبار ذلك في شبكات غير مؤمنة ، فيجب الاهتمام بقضايا الأمان بنفسك.



2. FFmpeg - قمت بتنزيل الثنائيات الجاهزة من Zeranoe builds



3.rav1e - يمكنك أيضًا تنزيل الملف الثنائي من إصدارات مشروع rav1e



نص PHP لكل كمبيوتر سيشارك
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";
?>




البرنامج النصي المحلي لتشغيل ترميز 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";
	}
}

?>






يكون الملف المراد تشغيل نص الترميز بجوار البرنامج النصي. يمكنك تكوين المسار إلى PHP بنفسك.

ترميز. 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


اذهب؟



للاختبار ، استخدمت الرسوم المتحركة الشهيرة Big Bucks Bunny حول أرنب ، بطول 10 دقائق وحجم 150 ميغابايت.



حديد



  • AMD Ryzen 5 1600 (12 خيطًا) + 16 جيجابايت DDR4 (Windows 10)
  • Intel Core i7 4770 (8 خيوط) + 32 جيجابايت DDR3 (Windows 10)
  • Intel Core i5 3570 (4 خيوط) + 8 جيجابايت DDR3 (Windows 10)
  • Intel Xeon E5-2650 V2 (16 موضوعًا) + 32 جيجابايت DDR3 (Windows 10)


المجموع: 40 موضوع



سطر الأوامر مع المعلمات



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


النتائج



مدة الترميز: 55 دقيقة

حجم الفيديو: 75 ميجا بايت



لن أتحدث عن الجودة ، لأن اختيار معلمات التشفير المثلى هو مهمة اليوم السابق ، واليوم تابعت هدف تحقيق وقت تشفير معقول ويبدو لي أنه نجح. كنت أخشى أن تلتصق القطع الملصقة ببعضها البعض بشكل سيئ وستحدث ارتعاشًا في هذه اللحظات ، لكن لا ، سارت النتيجة بسلاسة ، دون أي هزات.



بشكل منفصل ، لاحظت أن 1080p تتطلب حوالي غيغابايت من ذاكرة الوصول العشوائي لكل دفق ، لذلك يجب أن يكون هناك الكثير من الذاكرة. لاحظ أيضًا أنه في النهاية ، كان القطيع يعمل بسرعة أبطأ كبش ، وبينما انتهى كل من Ryzen و i7 منذ فترة طويلة من الترميز ، كان Xeon و i5 لا يزالان يتأرجحان على قطعهما. أولئك. سيتم ترميز مقطع فيديو أطول بشكل عام بمعدل إطارات في الثانية أعلى على حساب النوى الأسرع التي تقوم بمزيد من العمل.



عند إجراء التحويل على Ryzen 5 1600 مع تعدد مؤشرات الترابط ، كان الحد الأقصى الذي كنت أحصل عليه حوالي 1.5 إطارًا في الثانية. هنا ، بالنظر إلى أن الدقائق العشر الأخيرة من التشفير تنتهي من الأجزاء الأخيرة ذات النوى البطيئة ، يمكننا القول إنها تحولت إلى حوالي 5-6 إطارات في الثانية ، وهو ليس بالقليل بالنسبة لمثل هذا الترميز المتقدم. هذا كل ما أردت مشاركته ، وآمل أن يجده أحدهم مفيدًا.



All Articles