إضافات مصادر الوسائط

François Beaufort
François Beaufort
Joe Medley
Joe Medley

Media Source Extensions (MSE) هي واجهة برمجة تطبيقات JavaScript تتيح لك إنشاء أحداث بث لتشغيل مقاطع من الصوت أو الفيديو. على الرغم من أنّ هذه المقالة لا تتناول MSE، إلا أنّه يجب فهمه إذا كنت تريد تضمين فيديوهات في موقعك الإلكتروني تؤدي إجراءات مثل:

  • ميزة "البث التكيُّفي"، وهي طريقة أخرى للتعبير عن التكيُّف مع إمكانات الجهاز وظروف الشبكة
  • عمليات القطع والتلصيق التكيُّفية، مثل إدراج الإعلانات
  • تغيير الوقت
  • التحكّم في الأداء وحجم التنزيل
تدفق البيانات الأساسي في MSE
الشكل 1: تدفق بيانات MSE الأساسي

يمكنك اعتبار MSE سلسلة تقريبًا. كما هو موضّح في الشكل، هناك عدة طبقات بين الملف الذي تم تنزيله وعناصر الوسائط.

  • عنصر <audio> أو <video> لتشغيل الوسائط
  • مثيل MediaSource يحتوي على SourceBuffer لإدخال عنصر الوسائط
  • طلب fetch() أو XHR لاسترداد بيانات الوسائط في عنصر Response
  • مكالمة إلى Response.arrayBuffer() لإطعام MediaSource.SourceBuffer

في الواقع، تبدو السلسلة على النحو التالي:

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

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

ملاحظة حول الوضوح

هل ستوضّح لك هذه المقالة كل ما تحتاج إلى معرفته عن تشغيل الوسائط على صفحة ويب؟ لا، فالغرض منها هو مساعدتك فقط في فهم الرموز البرمجية الأكثر تعقيدًا التي قد تعثر عليها في مكان آخر. للتوضيح، يبسط هذا المستند العديد من الأمور ويطرحها خارج نطاق المناقشة. نعتقد أنّه يمكننا تنفيذ ذلك لأنّنا ننصحك أيضًا باستخدام مكتبة مثل Shaka Player من Google. سأشير في كل مكان إلى أين أبسّط المعلومات عمدًا.

بعض الأمور التي لا يشملها هذا الترخيص

في ما يلي بعض الأمور التي لن أتطرق إليها، بدون ترتيب معيّن:

  • عناصر التحكّم في التشغيل نحصل على هذه البيانات مجانًا من خلال استخدام عنصرَي HTML5 <audio> و<video>.
  • معالجة الأخطاء

للاستخدام في بيئات الإنتاج

في ما يلي بعض النقاط التي ننصح بها عند استخدام واجهات برمجة التطبيقات ذات الصلة بميزة "تحسين جودة الفيديو" في مرحلة الإنتاج:

  • قبل إجراء طلبات البيانات من واجهات برمجة التطبيقات هذه، عليك معالجة أي أحداث أخطاء أو استثناءات لواجهات برمجة التطبيقات، والتحقّق من HTMLMediaElement.readyState و MediaSource.readyState. ويمكن أن تتغيّر هذه القيم قبل إرسال الأحداث المرتبطة بها.
  • تأكَّد من أنّ المكالمات السابقة التي أجريتها مع appendBuffer() وremove() قد انتهت بنجاح من خلال التحقّق من القيمة المنطقية SourceBuffer.updating قبل تعديل mode أو timestampOffset أو appendWindowStart أو appendWindowEnd في SourceBuffer أو الاتصال بـ appendBuffer() أو remove() على SourceBuffer.
  • بالنسبة إلى جميع نُسخ SourceBuffer التي تمت إضافتها إلى MediaSource، تأكَّد من أنّه ليس هناك أي قيمة من قيم updating هي صحيحة قبل استدعاء MediaSource.endOfStream() أو تعديل MediaSource.duration.
  • إذا كانت قيمة MediaSource.readyState هي ended، ستؤدي الطلبات مثل appendBuffer() و remove() أو ضبط SourceBuffer.mode أو SourceBuffer.timestampOffset إلى نقل هذه القيمة إلى open. وهذا يعني أنّه يجب أن تكون مستعدًا للتعامل مع أحداث sourceopen متعددة.
  • عند التعامل مع أحداث HTMLMediaElement error، يمكن أن تكون محتويات MediaError.message مفيدة لتحديد السبب الأساسي للتعطّل، لا سيما بالنسبة إلى الأخطاء التي يصعب تكرارها في البيئات الاختبارية.

إرفاق مثيل MediaSource بعنصر وسائط

كما هو الحال مع العديد من الأمور في تطوير الويب هذه الأيام، عليك البدء بميزة الاكتشاف. بعد ذلك، احصل على عنصر وسائط، إما عنصر <audio> أو <video>. أخيرًا، أنشئ مثيلًا من MediaSource. ويتم تحويله إلى عنوان URL وتمريره إلى سمة مصدر عنصر الوسائط.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  // Is the MediaSource instance ready?
} else {
  console.log('The Media Source Extensions API is not supported.');
}
سمة مصدر بتنسيق ملف نصي
الشكل 1: سمة مصدر بتنسيق ملف نصي

قد يبدو من المريب نوعًا ما أنّه يمكن تمرير عنصر MediaSource إلى سمة src. وتكون عادةً سلاسل، ولكن يمكن أن تكون أيضًا مجموعات بيانات. إذا فحصت صفحة تتضمّن وسائط مضمّنة ودقّقت في عنصر الوسائط، ستفهم ما أقصده.

هل مثيل MediaSource جاهز؟

إنّ URL.createObjectURL() متزامن بحد ذاته، ومع ذلك، يعالج المرفق بشكل غير متزامن. يؤدي ذلك إلى تأخير طفيف قبل أن تتمكّن من إجراء أي إجراء باستخدام مثيل MediaSource. لحسن الحظ، هناك طرق لاختبار ذلك. إنّ أبسط طريقة هي استخدام خاصيّة MediaSource باسم readyState. يصف readyState العلاقة بين مثيل MediaSource وعنصر وسائط. يمكن أن يكون لها إحدى القيم التالية:

  • closed - لم يتم إرفاق مثيل MediaSource بعنصر وسائط.
  • open: تم إرفاق مثيل MediaSource بعنصر وسائط وهو جاهز لتلقّي البيانات أو يتلقّى البيانات.
  • ended: تم إرفاق مثيل MediaSource بعنصر وسائط وتم تمرير جميع بياناته إلى هذا العنصر.

يمكن أن يؤثّر طلب هذه الخيارات مباشرةً في الأداء سلبًا. لحسن الحظ، MediaSource تُطلق أيضًا الأحداث عند تغيُّر readyState، وتحديدًا sourceopen وsourceclosed وsourceended. في المثال الذي أعمل على إنشائه، سأستخدم الحدث sourceopen لإعلامي بالوقت المناسب لجلب المحتوى وتحميله مؤقتًا في ذاكرة التخزين المؤقت.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  <strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
  console.log("The Media Source Extensions API is not supported.")
}

<strong>function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  // Create a SourceBuffer and get the media file.
}</strong>

يُرجى العِلم أنّني تواصلتُ أيضًا مع revokeObjectURL(). أعلم أنّ هذا يبدو سابقًا لأوانه، ولكن يمكنني إجراء ذلك في أي وقت بعد ربط سمة src لعنصر الوسائط بمثيل MediaSource. لا يؤدي استدعاء هذه الطريقة إلى تدمير أي عناصر. يسمح هذا الإجراء للمنصة بمعالجة جمع المهملات في وقت مناسب، ولهذا السبب أطلب تنفيذه على الفور.

إنشاء SourceBuffer

حان الآن وقت إنشاء SourceBuffer، وهو العنصر الذي يؤدي عملية نقل البيانات بين مصادر الوسائط وعناصر الوسائط. يجب أن يكون SourceBufferمحددًا لنوع ملف الوسائط الذي تحمّله.

في الممارسة العملية، يمكنك إجراء ذلك من خلال استدعاء addSourceBuffer() باستخدام القيمة المناسبة. يُرجى ملاحظة أنّ سلسلة نوع MIME في المثال أدناه تحتوي على نوع MIME ومشفّرَين. هذه سلسلة mime لملف فيديو، ولكنها تستخدم برامج ترميز منفصلة لقسمَي الفيديو والصوت في الملف.

يسمح الإصدار 1 من مواصفات MSE لبرامج وكلاء المستخدمين بالاختلاف في ما إذا كان يجب طلب كل من نوع MIME وبرنامج ترميز. لا تشترط بعض برامج وكلاء المستخدمين استخدام نوع MIME، ولكنها تسمح به فقط. تتطلّب بعض برامج وكيل المستخدم، مثل Chrome، ترميزًا لأنواع mime التي لا تصف ترميزاتها ذاتيًا. بدلاً من محاولة فرز كل هذه العناصر، من الأفضل تضمين كليهما.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  <strong>
    var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
    the mediaSource instance. // Store it in a variable so it can be used in a
    closure. var mediaSource = e.target; var sourceBuffer =
    mediaSource.addSourceBuffer(mime); // Fetch and process the video.
  </strong>;
}

الحصول على ملف الوسائط

إذا أجريت بحثًا على الإنترنت عن أمثلة على MSE، ستجد الكثير من الأمثلة التي تسترجع ملفات الوسائط باستخدام XHR. لتكون أكثر حداثة، سأستخدم واجهة برمجة التطبيقات Fetch والوعد الذي تعرضه. إذا كنت تحاول إجراء ذلك في Safari، لن يعمل بدون استخدام مكتبة fetch() polyfill.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  <strong>
    fetch(videoUrl) .then(function(response){' '}
    {
      // Process the response object.
    }
    );
  </strong>;
}

سيحتوي مشغّل الإصدار العلني على الملف نفسه في إصدارات متعددة ليعمل على متصفّحات مختلفة. ويمكن أن يستخدم ملفات منفصلة للصوت والفيديو لسماح باختيار الصوت استنادًا إلى إعدادات اللغة.

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

معالجة عنصر الاستجابة

يبدو أنّ الرمز البرمجي مكتمل تقريبًا، ولكن لا يتم تشغيل الوسائط. نحتاج إلى الحصول على بيانات الوسائط من عنصر Response إلى SourceBuffer.

إنّ الطريقة المعتادة لنقل البيانات من عنصر الاستجابة إلى مثيل MediaSource هي الحصول على ArrayBuffer من عنصر الاستجابة ونقله إلى SourceBuffer. ابدأ بالاتصال بـ response.arrayBuffer()، الذي يعرض وعدًا في المخزن المؤقت. في الرمز البرمجي، تم تمرير هذا الوعد إلى عبارة then() ثانية حيث أضفته إلى SourceBuffer.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      <strong>return response.arrayBuffer();</strong>
    })
    <strong>.then(function(arrayBuffer) {
      sourceBuffer.appendBuffer(arrayBuffer);
    });</strong>
}

استدعاء endOfStream()

بعد إلحاق جميع ArrayBuffers وعدم توقّع وصول أي بيانات وسائط أخرى، اتصل بـ MediaSource.endOfStream(). سيؤدي ذلك إلى تغيير MediaSource.readyState إلى ended وإطلاق الحدث sourceended.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      <strong>sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });</strong>
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

النسخة النهائية

في ما يلي مثال على الرمز البرمجي الكامل. آمل أن تكون قد استفدت من هذه المقالة حول "إضافات مصدر الوسائط".

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

ملاحظات