הפעלה מהירה באמצעות טעינה מראש של אודיו ווידאו

איך להאיץ את הפעלת המדיה על ידי טעינה מראש של משאבים באופן פעיל.

François Beaufort
François Beaufort

התחלת הפעלה מהירה יותר תגרום לכך שיותר אנשים צופים בסרטון או מאזינים אודיו. זה עובדה ידועה. במאמר הזה אעבור על שיטות שבהן תוכל להשתמש כדי להאיץ את הפעלת האודיו והווידאו באמצעות שטוענים מראש משאבים בהתאם לתרחיש לדוגמה שלכם.

קרדיטים: זכויות יוצרים קרן בלנדר | www.blender.org .

אתאר שלוש שיטות לטעינה מראש של קובצי מדיה, נתחיל עם היתרונות שלהם וחסרונות

מעולה... אבל...
מאפיין הטעינה מראש של הסרטון פשוט לשימוש בקובץ ייחודי המתארח בשרת אינטרנט. דפדפנים עשויים להתעלם מהמאפיין לחלוטין.
אחזור המשאבים מתחיל כשמסמך ה-HTML נטען במלואו ו מנותחים.
תוספי מקור מדיה (MSE) מתעלמים מהמאפיין preload ברכיבי מדיה כי האפליקציה אחראית על מתן מדיה ל-MSE.
טעינה מראש של קישור המערכת מאלצת את הדפדפן לשלוח בקשה למשאב וידאו בלי לחסום אירוע onload של המסמך. בקשות HTTP Range לא תואמות.
תומך ב-MSE ומקטעי קבצים. מיועד לשימוש רק לקובצי מדיה קטנים (פחות מ-5MB) לאחזור משאבים מלאים.
אגירת נתונים ידנית שליטה מלאה טיפול בשגיאות מורכבות נמצא באחריות האתר.

מאפיין הטעינה מראש של הסרטון

אם מקור הווידאו הוא קובץ ייחודי המתארח בשרת אינטרנט, כדאי אולי מאפיין הווידאו 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:

  • כשחוסך הנתונים (Data Saver) מופעל, Chrome אוכף את הערך של preload none
  • ב-Android 4.3, Chrome אוכף את הערך preload ל-none עקב Android באג.
  • בחיבור לרשת סלולרית (2G, 3G ו-4G), Chrome מאלץ את הערך של preload metadata.

טיפים

אם האתר שלך מכיל משאבי סרטונים רבים באותו דומיין, מומלץ להגדיר את הערך של preload כ-metadata או להגדיר את poster ומגדירים את preload ל-none. כך תוכלו להימנע מפגיעה מספר מקסימלי של חיבורי HTTP לאותו דומיין (6 לפי מפרט HTTP 1.1), שיכול לעכב טעינה של משאבים. לתשומת ליבכם: לשפר את מהירות הדף אם הסרטונים אינם חלק מחוויית המשתמש העיקרית שלך.

כמו שמופיע במאמרים אחרים, טעינה מראש של קישור היא אחזור הצהרתי מאפשרת לאלץ את הדפדפן לשלוח בקשה למשאב חוסם את האירוע load ותוך כדי הורדת הדף. מקורות מידע שנטענו דרך <link rel="preload">, מאוחסנים באופן מקומי בדפדפן, באופן יעיל עד שיש הפניה אליהם במפורש ב-DOM, ב-JavaScript, או שירות CSS.

הטעינה מראש שונה משליפה מראש (prefetch) כי היא מתמקדת בניווט הנוכחי מאחזר משאבים עם עדיפות על סמך הסוג שלהם (סקריפט, סגנון, גופן, וידאו, אודיו וכו'). הוא משמש כדי להפעיל חימום של המטמון של הדפדפן במצב הנוכחי סשנים.

טעינה מראש של הסרטון המלא

כך תטען מראש סרטון מלא באתר שלכם, כדי קוד 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 וב-Service Workers, נבחן איך לבצע אגירת נתונים של סרטון באופן ידני באמצעות MSE. הדוגמה הבאה מבוססת על ההנחה שרשת האינטרנט שלכם השרת תומך ב-HTTP Range אבל זה יהיה די דומה כשמשתמשים בקובץ. פלחים. שימו לב שיש ספריות מתווכות כמו Saka של Google Player, 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. אנחנו לא משתמשים עדיין ב-service worker אפשר לגשת ל-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.
      });
    }
  });
}

יצירת תשובות בטווח בעזרת Service Worker

מה קורה אם אחזור של קובץ וידאו שלם ושמר אותו את Cache API? כשהדפדפן שולח בקשת HTTP Range, בהחלט לא שרוצים להעביר את הסרטון כולו לזיכרון של כלי הרינדור, כי Cache API יש תמיכה ב-Range תשובות עדיין.

אז אני רוצה להראות לך איך ליירט את הבקשות האלה ולהחזיר Range בהתאמה אישית תשובה מ-Service Worker.

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 בקשות.