كيفية تسريع تشغيل الوسائط من خلال التحميل المُسبَق للموارد بشكل نشط
يؤدي بدء التشغيل الأسرع إلى زيادة عدد الأشخاص الذين يشاهدون الفيديو أو يستمعون إليه. هذه حقيقة معروفة. في هذه المقالة، سأستكشف التقنيات التي يمكنك استخدامها لتسريع تشغيل الصوت والفيديو من خلال تحميل الموارد بشكل نشط استنادًا إلى حالة الاستخدام.
سأوضّح ثلاث طرق لتحميل ملفات الوسائط مسبقًا، بدءًا من مزاياها وعيوبه.
إنه رائع... | ولكن... | |
---|---|---|
سمة "التحميل المُسبَق للفيديو" | سهولة الاستخدام لملف فريد مستضاف على خادم ويب | وقد تتجاهل المتصفّحات السمة تمامًا. |
يبدأ جلب الموارد عندما يتم تحميل مستند HTML وتحليله بالكامل. | ||
تتجاهل إضافات مصدر الوسائط (MSE) سمة preload على عناصر الوسائط لأن التطبيق مسؤول عن توفير الوسائط إلى الخطأ التربيعي المتوسط.
|
||
التحميل المُسبق للربط |
تفرض هذه السياسة على المتصفِّح إرسال طلب للحصول على مورد فيديو بدون حظر
حدث onload في المستند.
|
طلبات نطاق HTTP غير متوافقة. |
متوافقة مع الخطأ التربيعي المتوسط وشرائح الملفات | يجب استخدام هذا الإجراء فقط مع ملفات الوسائط الصغيرة (<5 ميغابايت) عند جلب الموارد الكاملة. | |
التخزين المؤقت اليدوي | تحكُّم كامل | إنّ معالجة الأخطاء المعقدة هي من مسؤولية الموقع الإلكتروني. |
سمة التحميل المسبق للفيديو
إذا كان مصدر الفيديو ملفًا فريدًا مستضافًا على خادم ويب، ننصحك باستخدام سمة الفيديو preload
لتقديم تلميح للمتصفّح بشأن مقدار
المعلومات أو المحتوى الذي يجب تحميله مسبقًا. ويعني ذلك أنّ إضافات مصادر الوسائط
(MSE) غير متوافقة مع preload
.
لن يبدأ جلب المورد إلا بعد تحميل وتحليل مستند HTML الأولي بالكامل (مثلاً، بدء حدث DOMContentLoaded
)،
بينما سيتم بدء حدث load
المختلف تمامًا عند جلب المورد
فعليًا.
إنّ ضبط السمة preload
على metadata
يشير إلى أنّه من غير المتوقّع أن يحتاج المستخدم إلى الفيديو، لكنّه من المستحسن استرجاع بياناته الوصفية (السمات وقائمة الأغاني
والمدة وغيرها). يُرجى العلم بأنّه بدءًا من الإصدار Chrome 64، تكون القيمة التلقائية للسمة preload
هي metadata
. (كانت auto
سابقًا).
<video id="video" preload="metadata" src="file.mp4" controls></video>
<script>
video.addEventListener('loadedmetadata', function() {
if (video.buffered.length === 0) return;
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
});
</script>
يشير ضبط السمة preload
على auto
إلى أنّ المتصفّح قد يخزّن
ما يكفي من البيانات التي تمكّن التشغيل بدون التوقّف لزيادة
التخزين المؤقت.
<video id="video" preload="auto" src="file.mp4" controls></video>
<script>
video.addEventListener('loadedmetadata', function() {
if (video.buffered.length === 0) return;
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
});
</script>
ومع ذلك، هناك بعض الشروط. بما أنّ هذه مجرد إرشادات، قد يتجاهل المتصفّح السمة preload
بالكامل. في وقت كتابة هذا التقرير، إليك بعض القواعد
المطبّقة في Chrome:
- عند تفعيل توفير البيانات، يفرض Chrome القيمة
preload
علىnone
. - في Android 4.3، يفرض Chrome قيمة
preload
علىnone
بسبب خطأ في Android. - عند الاتصال بشبكة جوّال (شبكة الجيل الثاني والثالث والرابع)، يفرض Chrome قيمة
preload
علىmetadata
.
نصائح
إذا كان موقعك الإلكتروني يتضمّن العديد من مراجع الفيديو على النطاق نفسه، أقترح عليك
ضبط قيمة preload
على metadata
أو تحديد سمة poster
وضبط preload
على none
. وبهذه الطريقة، يمكنك تجنب بلوغ الحد الأقصى لعدد اتصالات HTTP في النطاق نفسه (6 وفقًا لمواصفات HTTP 1.1) والذي يمكنه تعليق تحميل الموارد. يُرجى العلم أنّ هذا الإجراء قد يؤدي أيضًا إلى
تحسين سرعة الصفحة إذا لم تكن الفيديوهات جزءًا من تجربة المستخدم الأساسية.
التحميل المسبق للرابط
كما تم توضيحه في مقالات أخرى، تحميل الروابط مسبقًا هو عملية جلب تعريفية تسمح
لك بإجبار المتصفّح على تقديم طلب لمورد بدون
حظر الحدث load
وأثناء تنزيل الصفحة. يتم تخزين الموارد
التي يتم تحميلها من خلال <link rel="preload">
محليًا في المتصفح، وتكون
غير نشطة بشكل فعّال إلى أن تتم الإشارة إليها بشكل صريح في DOM أو JavaScript
أو CSS.
يختلف التحميل المسبق عن الجلب المسبق في أنه يركز على التنقل الحالي ويجلب الموارد حسب الأولوية بناءً على نوعها (النص البرمجي والنمط والخط والفيديو والصوت وما إلى ذلك). ويجب استخدامه لتهيئة ذاكرة التخزين المؤقت للمتصفّح للجلسات الحالية.
التحميل المُسبق للفيديو الكامل
في ما يلي كيفية تحميل فيديو كامل مسبقًا على موقعك الإلكتروني لكي يتم قراءة المحتوى من ذاكرة التخزين المؤقت عندما يطلب ملف JavaScript جلب محتوى الفيديو، لأنّه قد يكون سبق للمتصفّح تخزينه مؤقتًا. إذا لم ينتهِ طلب التحميل المسبق بعد، سيتم تنفيذ عملية جلب منتظمة للشبكة.
<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">
<video id="video" controls></video>
<script>
// Later on, after some condition has been met, set video source to the
// preloaded video URL.
video.src = 'https://cdn.com/small-file.mp4';
video.play().then(() => {
// If preloaded video URL was already cached, playback started immediately.
});
</script>
وبما أنّ عنصر فيديو في المثال سيستهلك المورد المُحمَّل مُسبَقًا، تكون قيمة رابط التحميل المُسبق في as
هي video
. إذا كان عنصرًا صوتيًا، يكون as="audio"
.
تحميل الشريحة الأولى مسبقًا
يوضّح المثال أدناه كيفية التحميل المُسبق للمقطع الأول من فيديو باستخدام <link
rel="preload">
واستخدامه مع إضافات مصدر الوسائط. إذا لم تكن معتادًا على استخدام واجهة برمجة تطبيقات JavaScript للخطأ MSE، راجِع أساسيات الخطأ التربيعي المتوسط.
للتبسيط، لنفترض أنّه تم تقسيم الفيديو بالكامل إلى
ملفات أصغر مثل file_1.webm
وfile_2.webm
وfile_3.webm
وما إلى ذلك.
<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">
<video id="video" controls></video>
<script>
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
// If video is preloaded already, fetch will return immediately a response
// from the browser cache (memory cache). Otherwise, it will perform a
// regular network fetch.
fetch('https://cdn.com/file_1.webm')
.then(response => response.arrayBuffer())
.then(data => {
// Append the data into the new sourceBuffer.
sourceBuffer.appendBuffer(data);
// TODO: Fetch file_2.webm when user starts playing video.
})
.catch(error => {
// TODO: Show "Video is not available" message to user.
});
}
</script>
الدعم
يمكنك اكتشاف توافق عدة أنواع من as
مع <link rel=preload>
من خلال المقتطفات أدناه:
function preloadFullVideoSupported() {
const link = document.createElement('link');
link.as = 'video';
return (link.as === 'video');
}
function preloadFirstSegmentSupported() {
const link = document.createElement('link');
link.as = 'fetch';
return (link.as === 'fetch');
}
التخزين المؤقت اليدوي
قبل التعمّق في التفاصيل حول cache API وعاملي الخدمة، يجب معرفة كيفية تخزين فيديو مؤقتًا بشكل يدوي يحتوي على الخطأ التربيعي المتوسط. يفترض المثال أدناه أنّ
خادم الويب يتيح طلبات HTTP Range
، ولكن سيكون هذا مشابهًا جدًا لمقاطعملف
. يُرجى العِلم أنّ بعض مكتبات البرمجيات الوسيطة، مثل Shaka Player من Google وJW Player وVideo.js، مصمّمة لمعالجة هذه العملية نيابةً عنك.
<video id="video" controls></video>
<script>
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
// Fetch beginning of the video by setting the Range HTTP request header.
fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
.then(response => response.arrayBuffer())
.then(data => {
sourceBuffer.appendBuffer(data);
sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
});
}
function updateEnd() {
// Video is now ready to play!
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
// Fetch the next segment of video when user starts playing the video.
video.addEventListener('playing', fetchNextSegment, { once: true });
}
function fetchNextSegment() {
fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
.then(response => response.arrayBuffer())
.then(data => {
const sourceBuffer = mediaSource.sourceBuffers[0];
sourceBuffer.appendBuffer(data);
// TODO: Fetch further segment and append it.
});
}
</script>
الاعتبارات
بما أنك تتحكم الآن في تجربة التخزين المؤقت للوسائط بالكامل، أقترح عليك مراعاة مستوى بطارية الجهاز وخيار "وضع توفير البيانات" الذي يفضّله المستخدم ومعلومات الشبكة عند التفكير في التحميل المُسبق.
الوعي بالبطارية
ضَع في اعتبارك مستوى شحن بطارية أجهزة المستخدمين قبل التفكير في التحميل المُسبق للفيديو. سوف يحافظ هذا على عمر البطارية عندما يكون مستوى الطاقة منخفضًا.
يمكنك إيقاف التحميل المُسبق أو تحميل فيديو بدقة منخفضة على الأقل مسبقًا عند نفاد شحن بطارية الجهاز.
if ('getBattery' in navigator) {
navigator.getBattery()
.then(battery => {
// If battery is charging or battery level is high enough
if (battery.charging || battery.level > 0.15) {
// TODO: Preload the first segment of a video.
}
});
}
اكتشاف "توفير البيانات"
استخدِم عنوان طلب تلميح العميل Save-Data
لتقديم تطبيقات سريعة وخفيفة للمستخدمين الذين فعّلوا وضع "توفير البيانات" في المتصفّح. من خلال تحديد عنوان الطلب هذا، يمكن لتطبيقك تخصيص
تجربة مستخدم محدودة وتقديمها للمستخدمين المقيدين بالتكلفة والأداء.
راجِع تقديم تطبيقات سريعة وخفيفة باستخدام ميزة "حفظ البيانات" لمزيد من المعلومات.
التحميل الذكي استنادًا إلى معلومات الشبكة
قد تحتاج إلى التحقّق من navigator.connection.type
قبل التحميل المُسبَق. وعند ضبط السياسة على خيار "cellular
"، يمكنك منع التحميل المُسبق وإبلاغ المستخدمين بأنّ مشغِّل شبكة الجوّال قد يحصّل رسومًا مقابل معدّل نقل البيانات، ولا تبدأ سوى التشغيل التلقائي للمحتوى الذي تمّ تخزينه مؤقتًا في السابق فقط.
if ('connection' in navigator) {
if (navigator.connection.type == 'cellular') {
// TODO: Prompt user before preloading video
} else {
// TODO: Preload the first segment of a video.
}
}
اطّلِع على نموذج معلومات الشبكة للتعرّف على كيفية التفاعل مع التغييرات في الشبكة أيضًا.
التخزين المؤقت لعدة مقاطع أولى مسبقًا
ماذا لو أردت تحميل بعض محتوى الوسائط مسبقًا بشكل تخميني بدون معرفة المحتوى الذي سيختاره المستخدم في النهاية؟ إذا كان المستخدم يطالع صفحة ويب تتضمن 10 فيديوهات، من المحتمل أن تكون لدينا ذاكرة كافية لجلب
ملف مقطع واحد من كل منها، ولكن علينا بالتأكيد عدم إنشاء 10 عناصر <video>
مخفية
و10 كائنات MediaSource
والبدء في إضافة هذه البيانات إلى البيانات.
يوضح المثال أدناه المكوَّن من جزأين كيفية تخزين عدة مقاطع أولى من الفيديو مؤقتًا في ذاكرة التخزين المؤقت باستخدام واجهة cache API الفعّالة والسهلة الاستخدام. لاحظ أنه يمكن تحقيق شيء مماثل
باستخدام قاعدة البيانات المفهرسة. ولا نستخدم مشغِّلي الخدمات بعد لأنّ
واجهة برمجة تطبيقات ذاكرة التخزين المؤقت يمكن الوصول إليها أيضًا من العنصر window
.
الاسترجاع والتخزين المؤقت
const videoFileUrls = [
'bat_video_file_1.webm',
'cow_video_file_1.webm',
'dog_video_file_1.webm',
'fox_video_file_1.webm',
];
// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));
function fetchAndCache(videoFileUrl, cache) {
// Check first if video is in the cache.
return cache.match(videoFileUrl)
.then(cacheResponse => {
// Let's return cached response if video is already in the cache.
if (cacheResponse) {
return cacheResponse;
}
// Otherwise, fetch the video from the network.
return fetch(videoFileUrl)
.then(networkResponse => {
// Add the response to the cache and return network response in parallel.
cache.put(videoFileUrl, networkResponse.clone());
return networkResponse;
});
});
}
يُرجى العِلم أنّه إذا أردت استخدام طلبات HTTP Range
، سيكون عليّ إعادة إنشاء عنصر Response
يدويًا لأنّ Cache API لا تتيح استجابات Range
بعد. تذكَّر أنّ طلب networkResponse.arrayBuffer()
يؤدي إلى جلب محتوى الردّ بأكمله في آنٍ واحد إلى ذاكرة العارض، ولهذا السبب قد تحتاج إلى استخدام نطاقات صغيرة.
كمرجع لك، عدّلتُ جزءًا من المثال أعلاه لحفظ طلبات "نطاق HTTP" في ذاكرة التخزين المؤقت للفيديو.
...
return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
.then(networkResponse => networkResponse.arrayBuffer())
.then(data => {
const response = new Response(data);
// Add the response to the cache and return network response in parallel.
cache.put(videoFileUrl, response.clone());
return response;
});
تشغيل الفيديو
عندما ينقر المستخدم على زر التشغيل، سنسترجع المقطع الأول من الفيديو المتوفّر في واجهة برمجة التطبيقات Cache API لبدء التشغيل فورًا إذا كان متاحًا. وإلا، فإننا سنجلبه من الشبكة. ضع في اعتبارك أن المتصفحات والمستخدمين قد يقررون محو ذاكرة التخزين المؤقت.
كما رأينا سابقًا، نستخدم MSE لإدخال هذا المقطع الأول من الفيديو إلى عنصر الفيديو.
function onPlayButtonClick(videoFileUrl) {
video.load(); // Used to be able to play video later.
window.caches.open('video-pre-cache')
.then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
.then(response => response.arrayBuffer())
.then(data => {
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
sourceBuffer.appendBuffer(data);
video.play().then(() => {
// TODO: Fetch the rest of the video when user starts playing video.
});
}
});
}
إنشاء ردود النطاق باستخدام عامل خدمات
ماذا لو جلبت ملف فيديو كاملًا وحفظته في
Cache API؟ عندما يُرسِل المتصفّح طلب HTTP Range
، لا تريد بالتأكيد
نقل الفيديو بأكمله إلى ذاكرة أداة التحويل، لأنّ واجهة برمجة التطبيقات Cache API لا تسمح بعد باستخدام استجابات Range
.
سأوضّح لك طريقة اعتراض هذه الطلبات وعرض ردّ Range
مخصّص من مشغّل الخدمات.
addEventListener('fetch', event => {
event.respondWith(loadFromCacheOrFetch(event.request));
});
function loadFromCacheOrFetch(request) {
// Search through all available caches for this request.
return caches.match(request)
.then(response => {
// Fetch from network if it's not already in the cache.
if (!response) {
return fetch(request);
// Note that we may want to add the response to the cache and return
// network response in parallel as well.
}
// Browser sends a HTTP Range request. Let's provide one reconstructed
// manually from the cache.
if (request.headers.has('range')) {
return response.blob()
.then(data => {
// Get start position from Range request header.
const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
const options = {
status: 206,
statusText: 'Partial Content',
headers: response.headers
}
const slicedResponse = new Response(data.slice(pos), options);
slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
(data.size - 1) + '/' + data.size);
slicedResponse.setHeaders('X-From-Cache': 'true');
return slicedResponse;
});
}
return response;
}
}
من المهم التنويه بأنّني استخدمتُ response.blob()
لإعادة إنشاء هذه الاستجابة المقسّمة، لأنّ ذلك يساعدني ببساطة في التعامل مع الملف، بينما يضع response.arrayBuffer()
الملف بأكمله في ذاكرة العارض.
يمكن استخدام عنوان HTTP المخصّص X-From-Cache
لمعرفة ما إذا كان مصدر هذا الطلب
من ذاكرة التخزين المؤقت أو الشبكة. ويمكن أن يستخدمه مشغّل مثل ShakaPlayer لتجاهل وقت الاستجابة كمؤشر على سرعة الشبكة.
ألقِ نظرة على Sample Media App الرسمي، لا سيما ملف ranged-response.js المخصّص له للاطّلاع على حلّ كامل لكيفية معالجة طلبات Range
.