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