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

فرانسوا بوفورت
"فرانسوا بوفورت"
جو ميدلي
جو ميدلي

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

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

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

  • عنصر <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);
    });
}

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

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

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

بعض العناصر التي لم يتم تناولها

في ما يلي بعض الأمور التي لن أتناولها، بدون ترتيب محدّد.

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

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

وفي ما يلي بعض النصائح التي أنصحك بها عند استخدام واجهات برمجة التطبيقات ذات الصلة بالخطأ التربيعي المتوسط (MSE):

  • قبل طلب البيانات من واجهات برمجة التطبيقات هذه، يُرجى معالجة أي أحداث أخطاء أو استثناءات من واجهة برمجة التطبيقات والتحقّق من 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>;
}

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

إذا أجريت بحثًا على الإنترنت عن أمثلة الخطأ التربيعي المتوسط، فستجد الكثير الذي يسترد ملفات الوسائط باستخدام 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);
    });
}

إضافة ملاحظات