Media Source Extensions (MSE) הוא ממשק JavaScript API שמאפשר ליצור סטרימינג להפעלה מקטעים של אודיו או וידאו. במאמר הזה לא נסביר על MSE, אבל חשוב להבין את הנושא אם אתם רוצים להטמיע באתר סרטונים שמאפשרים לבצע פעולות כמו:
- סטרימינג אדפטיבי, כלומר התאמה ליכולות המכשיר ולתנאי הרשת
- חיבור מותאם, כמו הוספת מודעות
- שינוי מועד השידור
- שליטה בביצועים ובגודל ההורדה
![זרימת נתונים בסיסית של MSE](https://web.developers.google.cn/static/articles/media-mse-basics/image/basic-mse-data-flow-9f377a6841412.png?authuser=8&hl=he)
אפשר לחשוב על 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
. הערכים האלה עשויים להשתנות לפני שהאירועים המשויכים יועברו. - לפני שמעדכנים את
mode
,timestampOffset
,appendWindowStart
אוappendWindowEnd
שלSourceBuffer
, או לפני שמפעילים אתappendBuffer()
אוremove()
ב-SourceBuffer
, צריך לוודא שהקריאות הקודמות שלappendBuffer()
ו-remove()
לא נמצאות עדיין בתהליך. לשם כך, בודקים את הערך הבוליאניSourceBuffer.updating
. - בכל המופעים של
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](https://web.developers.google.cn/static/articles/media-mse-basics/image/a-source-attribute-a-blo-8b1f76f582a22.png?authuser=8&hl=he)
יכול להיות שייראה לכם מוזר שאפשר להעביר אובייקט 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
. קריאה לשיטה הזו לא גורמת להשמדה של אובייקטים. היא כן מאפשרת לפלטפורמה לטפל באיסוף האשפה בזמן המתאים, ולכן אני קורא לה באופן מיידי.
יצירת 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. כדי להשתמש בטכנולוגיה מתקדמת יותר, אשתמש ב-API של Fetch וב-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);
});
}