يمكنك تشغيل الفيديو بسرعة من خلال تحميل الصوت والفيديو مسبقًا.

كيفية تسريع تشغيل الوسائط من خلال التحميل المُسبَق للموارد بشكل نشط

François Beaufort
François Beaufort

كلما بدأ تشغيل الفيديو بشكل أسرع، زاد عدد المستخدمين الذين يشاهدون الفيديو أو يستمعون إلى المحتوى الموسقي. هذه حقيقة معروفة. في هذه المقالة، سأستكشف التقنيات التي يمكنك استخدامها لتسريع تشغيل الصوت والفيديو من خلال تحميل الموارد بشكلٍ نشط استنادًا إلى حالة الاستخدام.

معلومات حقوق الطبع والنشر: حقوق الطبع والنشر لمؤسسة Blender | www.blender.org .

سأوضّح ثلاث طرق لتحميل ملفات الوسائط مسبقًا، بدءًا من مزاياها وعيوبه.

رائع... ولكن...
سمة "التحميل المُسبَق للفيديو" سهولة الاستخدام لملف فريد مستضاف على خادم ويب وقد تتجاهل المتصفّحات السمة بالكامل.
تبدأ عملية جلب الموارد عند اكتمال تحميل ملف HTML وتحليله بالكامل.
تتجاهل "إضافات مصدر الوسائط" (MSE) سمة preload في عناصر الوسائط لأنّ التطبيق هو المسؤول عن تقديم الوسائط إلى MSE.
تحميل الرابط مسبقًا تجبر هذه السمة المتصفّح على تقديم طلب لمورد فيديو بدون حظر حدث onload في المستند. طلبات النطاق في HTTP غير متوافقة.
متوافق مع MSE وشرائح الملفات يجب استخدام هذا الإجراء فقط مع ملفات الوسائط الصغيرة (<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.

يختلف التثبيت المُسبَق عن التثبيت المُسبَق المُعدّ مسبقًا من حيث أنّه يركز على التنقّل الحالي وي retrieving يُسترجع الموارد بأولوية استنادًا إلى نوعها (النص البرمجي أو النمط أو الخط أو الفيديو أو الصوت أو غير ذلك). ويجب استخدامه لتهيئة ذاكرة التخزين المؤقت للمتصفّح للجلسات الحالية.

تحميل الفيديو بالكامل مسبقًا

في ما يلي كيفية تحميل فيديو كامل مسبقًا على موقعك الإلكتروني لكي يتم قراءة المحتوى من ذاكرة التخزين المؤقت عندما يطلب ملف 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"> واستخدامه مع إضافات مصدر الوسائط. إذا لم تكن على دراية بواجهة برمجة التطبيقات MSE JavaScript API، يمكنك الاطّلاع على أساسيات 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 وخدمات العمال، لنطّلِع على كيفية تخزين فيديو يدويًا باستخدام MSE. يفترض المثال أدناه أنّ خادم الويب يتيح طلبات 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 القوية والسهلة الاستخدام. تجدر الإشارة إلى أنّه يمكن أيضًا تنفيذ إجراء مماثل باستخدام IndexedDB. لا نستخدم مهام الخدمة بعد لأنّه يمكن أيضًا الوصول إلى واجهة برمجة التطبيقات 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;
    });
  });
}

يُرجى العِلم أنّه إذا كنت سأستخدم طلبات Range عبر HTTP، سأحتاج إلى إعادة إنشاء عنصر Response يدويًا لأنّ واجهة برمجة التطبيقات Cache API لا تتيح استخدام استجابات Range حتى الآن. يُرجى إعلام بأنّ استدعاء networkResponse.arrayBuffer() يُجلب المحتوى بالكامل للاستجابة في آنٍ واحد إلى ذاكرة المشغّل، ولهذا السبب قد تحتاج إلى استخدام نطاقات صغيرة.

كمرجع، عدّلت جزءًا من المثال أعلاه لحفظ طلبات HTTP Range في ميزة "التخزين المؤقت المُسبَق" للفيديو.

    ...
    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 لتجاهل وقت الاستجابة كمؤشر على سرعة الشبكة.

اطّلِع على نموذج تطبيق الوسائط الرسمي، وعلى ملف ranged-response.js على وجه الخصوص للحصول على حلّ كامل حول كيفية معالجة طلبات Range.