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

פרנסואה בופורט
פרנסואה בופורט
ג'ו מדלי
ג'ו מדלי

Media Source extensions (MSE) הוא ממשק API של 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. כל אחד משלבי ה-build יוסיף קוד לשלב הקודם.

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

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

כמה דברים שלא מכוסים

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

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

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

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

  • לפני שמבצעים קריאות לממשקי ה-API האלה, צריך לטפל באירועי שגיאה או בהחרגות של ה-API, ולבדוק את HTMLMediaElement.readyState ו-MediaSource.readyState. הערכים האלה עשויים להשתנות לפני שהאירועים המשויכים נשלחים.
  • כדאי לוודא שהשיחות הקודמות ב-appendBuffer() וב-remove() לא מתבצעות עדיין. לשם כך, צריך לבדוק את הערך הבוליאני של SourceBuffer.updating לפני שמעדכנים את ה-mode, timestampOffset, appendWindowStart, appendWindowEnd של SourceBuffer, או מתקשרים אל appendBuffer() או remove() ב-SourceBuffer.
  • לפני שקוראים MediaSource.endOfStream() או מעדכנים את התג MediaSource.duration, צריך לוודא updating לכל המכונות שמוסיפים SourceBuffer ל-MediaSource.
  • אם הערך של MediaSource.readyState הוא ended, שיחות כמו appendBuffer() ו-remove(), או הגדרה של SourceBuffer.mode או SourceBuffer.timestampOffset, יגרמו למעבר של הערך הזה אל open. פירוש הדבר הוא שכדאי להיות מוכנים לטפל בכמה אירועי sourceopen.
  • כשמטפלים באירועי HTMLMediaElement error, התוכן של MediaError.message יכול לעזור בקביעת שורש הבעיה, במיוחד בשגיאות שקשה לשחזר בסביבות בדיקה.

צירוף מופע של MediaSource לרכיב מדיה

כמו הרבה דברים אחרים בפיתוח אתרים כיום, השלב הראשון הוא זיהוי תכונות. בשלב הבא, צריך להוריד רכיב מדיה, <audio> או <video>. לבסוף, יוצרים מופע של MediaSource. היא הופכת לכתובת אתר ומועברת למאפיין המקור של רכיב המדיה.

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

האם מופע ה-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 צריך להיות ספציפי לסוג של קובץ המדיה שניסית לטעון.

בפועל, אפשר לבקש מ-addSourceBuffer() עם הערך המתאים. שימו לב שבדוגמה שלמטה המחרוזת של סוג ה-MIME מכילה סוג MIME ו-שני רכיבי Codec. זוהי מחרוזת 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 API וב-Promise שהוא מחזיר. אם אתם מנסים לעשות זאת ב-Safari, לא תוכלו לבצע זאת ללא מילוי אוטומטי של 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);
    });
}

משוב