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

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

François Beaufort
François Beaufort

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

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

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

إنّه رائع... ولكن...
سمة التحميل المُسبق للفيديو ويسهل استخدامها لملف فريد تتم استضافته على خادم ويب. وقد تتجاهل المتصفّحات السمة تمامًا.
يبدأ جلب الموارد عندما يتم تحميل مستند 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، راجِع أساسيات الخطأ السوقي للخطأ (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');
}

التخزين المؤقت اليدوي

قبل أن نتعمق في واجهة برمجة تطبيقات ذاكرة التخزين المؤقت وعاملي الخدمة، يمكننا طريقة التخزين المؤقت اليدوي للفيديوهات التي تتضمّن الخطأ المتوسط. يفترض المثال التالي أن موقعك الإلكتروني الخادم يدعم HTTP Range ولكن سيكون ذلك متشابهًا إلى حد كبير مع الأقسام. يُرجى العلم أنّ بعض مكتبات البرمجيات الوسيطة مثل shaka من 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 لكي يبدأ التشغيل فورًا في حال توفّرها. وإلا، فإننا سنجلبه من الشبكة. ضع في اعتبارك أن المتصفحات وقد يقرر المستخدمون محو ذاكرة التخزين المؤقت.

وكما ذكرنا سابقًا، نستخدم الخطأ التربيعي المتوسط لإضافة المقطع الأول من الفيديو إلى الفيديو. العنصر.

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.
      });
    }
  });
}

إنشاء ردود النطاق باستخدام عامل خدمات

ماذا لو كنت قد جلبت ملف فيديو كاملًا وحفظته في واجهة برمجة التطبيقات لذاكرة التخزين المؤقت؟ عندما يرسل المتصفّح طلب 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 الطلبات.