תוספים של מקור מדיה

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

Media Source Extensions‏ (MSE) הוא ממשק JavaScript API שמאפשר ליצור סטרימינג להפעלה מקטעים של אודיו או וידאו. למרות שלא מופיע במאמר הזה, נדרשת הבנה של 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. כל אחד משלביו של תהליך ה-build יוסיף קוד לשלב הקודם.

הערה בנושא הבהרה

האם במאמר הזה נסביר לכם את כל מה שצריך לדעת על הפעלת מדיה בדף אינטרנט? לא, היא נועדה רק לעזור לכם להבין קוד מורכב יותר שאולי תמצאו במקומות אחרים. לשם הבהרה, המסמך הזה מפשט דברים רבים ומחריג אותם. אנחנו סבורים שאנחנו יכולים לעשות זאת כי אנחנו ממליצים גם להשתמש בספרייה כמו Shaka Player של Google. לאורך הדרך יצוינו המקומות שבהם פשוטתי את הדברים בכוונה.

כמה דברים שלא נכללים

לפניכם כמה נושאים, ללא סדר מסוים, שלא אעסוק בהם.

  • רכיבי UI להפעלה. אנחנו מקבלים אותן בחינם בזכות השימוש ברכיבי ה-HTML5 <audio> ו-<video>.
  • טיפול בשגיאות.

לשימוש בסביבות ייצור

הנה כמה דברים שאני ממליץ עליהם לשימוש בסביבת הייצור של ממשקי API שקשורים ל-MSE:

  • לפני שמבצעים קריאות ל-API האלה, צריך לטפל באירועי שגיאה או בחריגות ב-API ולבדוק את HTMLMediaElement.readyState ו-MediaSource.readyState. הערכים האלה עשויים להשתנות לפני שהאירועים המשויכים יועברו.
  • כדי לוודא שהשיחות הקודמות של appendBuffer() ו-remove() עדיין לא מתבצעות, בודקים את הערך הבוליאני SourceBuffer.updating לפני שמעדכנים את mode, timestampOffset, appendWindowStart ו-appendWindowEnd של SourceBuffer, או קוראים ל-appendBuffer() או remove() ב-SourceBuffer.
  • בכל המופעים של SourceBuffer שנוספו ל-MediaSource, צריך לוודא שאף אחד מהערכים של updating לא שווה ל-true לפני שמפעילים את 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.');
}
מאפיין מקור כ-blob
איור 1: מאפיין מקור כ-blob

יכול להיות שייראה לכם מוזר שאפשר להעביר אובייקט MediaSource למאפיין src. בדרך כלל הם מחרוזות, אבל הם יכולים להיות גם blobs. אם תבדקו דף שיש בו מדיה מוטמעת ותבחנו את רכיב המדיה שלו, תוכלו להבין למה אני מתכוון.

האם המכונה של 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. קריאה ל-method הזו לא גורמת להשמדה של אובייקטים. זה מאפשר לפלטפורמה לטפל באיסוף אשפה בזמן מתאים, ולכן אתקשר אליך מיד.

יצירת חוצץ SourceBuffer

עכשיו הגיע הזמן ליצור את SourceBuffer, שהוא האובייקט שבעצם מבצע את העבודה של העברת הנתונים בין מקורות המדיה לבין רכיבי המדיה. הערך של SourceBuffer צריך להיות ספציפי לסוג קובץ המדיה שאתם מעלים.

בפועל, אפשר לעשות זאת על ידי קריאה ל-addSourceBuffer() עם הערך המתאים. שימו לב שבדוגמה שלמטה, המחרוזת של סוג mime מכילה סוג mime ו-שני קודקים. זוהי מחרוזת mime לקובץ וידאו, אבל היא משתמשת ברכיבי קודק נפרדים לחלקי הווידאו והאודיו בקובץ.

בגרסה 1 של מפרט ה-MSE, סוגי ה-User Agent יכולים להשתנות לגבי הדרישה לסוג MIME ולקודק. סוגי User Agent מסוימים לא דורשים סוג 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 API וה-Promise שהוא יחזיר. אם מנסים לעשות זאת ב-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);
    });
}

משוב