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

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 من مواصفات الخطأ التربيعي المتوسط لوكلاء المستخدم بالاختلاف في ما إذا كان يجب طلب كل من نوع 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>;
}

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

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

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);
    });
}

ملاحظات