استفاده از ورکرهای Node.js برای انکود کردن ویدئو — از صفر تا صد
انکود کردن ویدئو مصرف CPU بالایی دارد. در گذشته Node.js هرگز برای اجرای وظایفی که مصرف CPU بالایی دارند، مناسب محسوب نمیشد. دلیل این مسئله آن است که Node.js از یک نخ منفرد برای اجرای کد جاوا اسکریپت استفاده میکند. اما از زمان انتشار نسخه 11.7.0 Node.js یک ماژول جدید به نام worker_threads به آن اضافه شده است. در این مقاله با روش استفاده از ورکرهای Node.js برای انکود کردن ویدئو آشنا میشویم.
ورکرها را میتوانیم به صورت زیر توصیف کنیم:
ورکرها (نخها) را میتوان برای اجرای عملیات جاوا اسکریپت که از نظر مصرف CPU سنگین است، مورد استفاده قرار داد. این ورکرها به کارهایی که عملیات I/O سنگین دارند کمک چندانی نمیکنند. عملیات داخلی ورودی-خروجی ناهمگام Node.js بسیار بهینهتر از ورکرها است.
به همین جهت تلاش میکنیم در ادامه این مطلب یک کلاس انکودینگ ویدئو به نام Workflow Encoder را با استفاده از ورکرها پیادهسازی کنیم.
انکود کردن ویدئو
پیش از توصیف پیادهسازی این انکودر باید کمی در مورد این که چرا انکودینگ برای استریم کردن ویدئو ضروری است توضیح دهیم.
Workflow Encoder بخشی از یک پروژه بزرگتر به نام Mini Video Encoder (+) به اختصار MVE است. MVE یک پلتفرم اوپن سورس برای تبدیل ویدئوها محسوب میشود. پس از تبدیل، یک سرور معمولی HTTP میتواند ویدئوها را با استفاده از استریمینگ تطبیقی تحویل دهد.
استریمینگ تطبیقی چیست؟
«استریمینگ تطبیقی» (Adaptive Streaming) نرخ بیت و وضوح تصویر یک ویدئو را در طی بازپخش تغییر میدهد. یک پخشکننده ویدئو به طور پیوسته پهنای باند اتصال را اندازهگیری میکند و کیفیت ویدئو را بسته به این شاخص کاهش یا افزایش میدهد.
برای عملیاتی ساختن این ایده Workflow Encoder نسخههای متعددی از یک ویدئو میسازد. هر نسخه دارای نرخ بیت و وضوح متفاوتی است. این فهرست نرخهای بیت و وضوحها به نام encoding ladder خوانده میشوند.
اپل encoding ladder زیر را برای یک ویدئوی 1080p توصیه میکند. اگر ویدئوهایتان را انکود میکنید از این ladder استفاده کنید تا ویدئو روی دستگاههای اپل به درستی پخش میشود.
استفاده از ladder به این معنی است که Workflow Encoder باید 9 انکودینگ متفاوت ایجاد کند. بدین ترتیب با دلیل این که چرا باید از بهینهترین روش برای انکودینگ ویدئو استفاده کنیم، آشنا شدید. Workflow Encoder از FFmpeg 4.2.2 استفاده میکند. FFmpeg یک انکودر صوت و ویدئوی اوپن سورس است. همچنین از Fluent ffmpeg-API برای تسهیل تعامل با FFmpeg استفاده میکند. ما به منظور تست از یک نسخه 1080p از ویدئوی Caminandes 3: Llamigos استفاده میکنیم:
Caminandes 3 (+) یک ویدئوی انیمیشن اوپن سورس 2.5 دقیقهای از بنیاد بلندر (Blender) است.
پیادهسازی Workflow Encoder با استفاده از ورکرها
زمانی که موتور Workflow یک کار ویدئویی دریافت میکند، ابتدا آن کار را افراز میکند. برای هر ترکیب نرخ بیت و وضوح تصویر از encoding ladder موتور Workflow یک وظیفه تعریف میشود:
انکودر Workflow با موتور Workflow تعامل مییابد و سؤال میکند آیا وظیفهای برای اجرا وجود دارد یا نه. انکودر Workflow از REST API برای ارتباط با موتور Workflow کمک میگیرد.
ایجاد و آغاز یک ورکر
اگر یک وظیفه برای اجرا وجود داشته باشد، انکودر Workflow اقدام به فراخوانی تابع startEncoder میکند. تابع startEncoder یک ورکر را ایجاد و آغاز میکند. این تابع شیء Worker را با فراخوانی سازنده و ارسال یک مسیر نسبی به فایل جاوا اسکریپت ایجاد میکند. این فایل شامل تابعی است که باید روی نخ متفاوتی اجرا شود:
1/*
2 * Encoder Engine
3 */
4
5// Dependencies
6const workerThreads = require('worker_threads');
7const superagent = require('superagent');
8
9const log = require('./log');
10const constants = require('./constants');
11const config = require('./config');
12
13const encoderEngine = {};
14
15encoderEngine.startEncoder = function startEncoder(encodingInstructions, cb) {
16 log.info('Starting Worker....');
17
18 const worker = new workerThreads.Worker('./lib/encoder/encoder.js', {
19 workerData: encodingInstructions,
20 });
21};
آرگومان دوم سازنده Worker یک شیء options است. ما از این شیء برای تعیین workerData روی encodingInstructions استفاده میکنیم. این انتساب موجب کلون شدن encodingInstructions میشود و آن را در اختیار تابع ورکر قرار میدهد.
تابع واقعی که کار را انجام خواهد داد، تابع encode در فایل encoder.js است. این فایل شامل تابع منفردی است که ورکر اجرا میکند. بخشهای زیادی را حذف کردیم تا تمرکز روی بخش اصلی قرار بگیرد.
1const { parentPort, workerData } = require("worker_threads");
2
3async function encode() {
4
5 const encodingInstructions = workerData;
6
7 ...
8
9}
10
11encode();
در خط 5 با خواندن workerData مورد encodingInstructions را به دست میآوریم. در آغاز فایل، parentPort را نیز require میکنیم که تابع از آن برای برقراری ارتباط با نخ اصلی و نخ ورکر استفاده می کند.
ارتباط از ورکر با نخ اصلی
ماژول ورکر امکان ارتباط دوطرفه را بین نخ اصلی و نخ ورکر فراهم میسوزد. ما علاقهمند هستیم که از نخ ورکر روی نخ اصلی در مورد پیشرفت کار بازخوردی دریافت کنیم.
1const message = {};
2message.type = constants.WORKER_MESSAGE_TYPES.PROGRESS;
3message.message = `Encoding: ${Math.round(info.percent)}%`;
4parentPort.postMessage(message);
در اغلب مثالها از postMessage برای ارسال یک رشته استفاده شده است. در عوض ما یک شیء را مبادله میکنیم. بدین ترتیب پیامهای متفاوتی را میفرستیم و میتوانیم آنها را در نخ اصلی از هم متمایز سازیم.
از سوی دیگر نخ اصلی این پیامها را از طریق رویدادها به دست میآورد. در خط 8، worker.on تابعی را تعریف میکند که رویدادهای پیام را دریافت میکند.
1encoderEngine.startEncoder = function startEncoder(encodingInstructions, cb) {
2 log.info('Starting Worker....');
3
4 const worker = new workerThreads.Worker('./lib/encoder/encoder.js', {
5 workerData: encodingInstructions,
6 });
7
8 worker.on('message', (message) => {
9 if (message != null && typeof message === 'object') {
10 if (message.type === constants.WORKER_MESSAGE_TYPES.PROGRESS) {
11 log.info(message.message);
12 } else if (message.type === constants.WORKER_MESSAGE_TYPES.ERROR) {
13 cb(new Error(message.message));
14 } else if (message.type === constants.WORKER_MESSAGE_TYPES.DONE) {
15 log.info(message.message);
16 cb(null);
17 }
18 }
19 });
20
21 worker.on('error', (err) => {
22 cb(new Error(`An error occurred while encoding. ${err}`));
23 });
24
25 worker.on('exit', (code) => {
26 if (code !== 0) {
27 cb(new Error(`Worker stopped with exit code ${code}`));
28 } else {
29 cb(null);
30 }
31 });
32};
بسته به نوع پیام، تابع یک اکشن خاص را اجرا میکند. در مورد پیام PROGRESS، این پیام را با استفاده از شیء log لاگ میکند. بدین طریق پیشرفت کار ورکر را در نخ اصلی نیز میبینیم:
برقراری ارتباط از نخ اصلی به ورکر
گاهی اوقات لازم است از سمت دیگر یعنی از نخ اصلی به ورکر نیز ارتباط برقرار کنیم. برای نمونه فرض کنید میخواهیم یک وظیفه انکودینگ در حال اجرا را متوقف سازیم. مکانیسم کار تقریباً همانند برقراری ارتباط از ورکر به نخ اصلی است. در این حالت از متد postMessage روی شیء worker استفاده میکنیم.
1encoderEngine.stopEncoder = function stopEncoder(worker) {
2
3 const message = {};
4 message.type = constants.WORKER_MESSAGE_TYPES.STOP_ENCODING;
5 worker.postMessage(message);
6
7}
ورکر از parentPort برای ایجاد یک دستگیره رویداد برای دریافت پیامها بهره میگیرد:
1parentPort.on('message', (message) => {
2 if (message.type === constants.WORKER_MESSAGE_TYPES.STOP_ENCODING) {
3 // Main thread asks to kill this thread.
4 log.info(`Main thread asked to stop this thread`);
5 ffmpegCommand.kill();
6 }
7});
زمانی که ورکر یک STOP_ENCODING دریافت میکند، اجرای وظیفه انکودینگ را متوقف میسازد. این وظیفه با فراخوانی ffmpegCommand.kill() متوقف میشود. بدین ترتیب SIGKILL به پردازش FFmpeg ارسال شده و آن را متوقف میسازد.
سخن پایانی
تیم Node.js بحث نخبندی را با استفاده از ورکرها به خوبی پیادهسازی کردهاند. ما با داشتن یک کانال ارتباطی خاص بین نخها از مشکلات «همگامسازی» (Synchronization) اجتناب میکنیم. همگامسازی موجب بروز مشکلات زیادی در زبانهای برنامهنویسی دیگر شده است. در پیادهسازی کنونی Workflow Encoder از ورکرها برای انکود کردن ویدئو استفاده شده است. کد سورس این پروژه را میتوانید در این ریپوی گیتهاب (+) ملاحظه کنید. البته همچنان در حال تکمیل شدن است.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش Node.js — مجموعه مقالات مجله فرادرس
- آموزش Node.js: آشنایی با استریمها و کار با MySQL — بخش دوازدهم
- آموزش Node.js: مفاهیم مقدماتی — بخش اول
==