เล่นอย่างรวดเร็วด้วยเสียงและวิดีโอที่โหลดไว้ล่วงหน้า

วิธีเร่งการเล่นสื่อด้วยการโหลดทรัพยากรล่วงหน้าอย่างสม่ำเสมอ

François Beaufort
François Beaufort

การเริ่มเล่นที่เร็วขึ้นหมายความว่ามีผู้คนจำนวนมากขึ้นที่ดูวิดีโอหรือฟังเพลง เสียง เป็นข้อเท็จจริงที่รู้อยู่แล้ว ในบทความนี้ เราจะพูดถึง เทคนิคที่คุณสามารถใช้เพื่อเร่งการเล่นเสียงและวิดีโอ ด้วยการ การโหลดทรัพยากรล่วงหน้าโดยขึ้นอยู่กับกรณีการใช้งาน

เครดิต: ลิขสิทธิ์ Blender Foundation | www.blender.org

ฉันจะอธิบายวิธีการโหลดไฟล์สื่อล่วงหน้า 3 วิธี โดยเริ่มจากมืออาชีพ และข้อเสีย

เยี่ยมมาก... แต่ว่า...
แอตทริบิวต์การโหลดวิดีโอล่วงหน้า ใช้งานง่ายสำหรับไฟล์ที่ไม่ซ้ำกันที่โฮสต์ในเว็บเซิร์ฟเวอร์ เบราว์เซอร์อาจละเว้นแอตทริบิวต์นี้โดยสิ้นเชิง
การดึงข้อมูลทรัพยากรจะเริ่มต้นเมื่อโหลดเอกสาร HTML เสร็จสมบูรณ์ และ แยกวิเคราะห์แล้ว
ส่วนขยายแหล่งที่มาของสื่อ (MSE) จะไม่สนใจแอตทริบิวต์ preload ในองค์ประกอบสื่อเนื่องจากแอปเป็นผู้รับผิดชอบ การจัดหาสื่อให้กับ MSE
การโหลดลิงก์ล่วงหน้า บังคับให้เบราว์เซอร์ส่งคำขอทรัพยากรวิดีโอโดยไม่บล็อก เหตุการณ์ onload ของเอกสาร คำขอช่วง HTTP ใช้ร่วมกันไม่ได้
เข้ากันได้กับ MSE และกลุ่มไฟล์ ควรใช้สำหรับไฟล์สื่อขนาดเล็ก (<5 MB) เมื่อดึงข้อมูลทรัพยากรทั้งหมดเท่านั้น
การบัฟเฟอร์ด้วยตนเอง ควบคุมได้เต็มรูปแบบ การจัดการข้อผิดพลาดที่ซับซ้อนถือเป็นความรับผิดชอบของเว็บไซต์

แอตทริบิวต์การโหลดวิดีโอล่วงหน้า

หากแหล่งที่มาของวิดีโอเป็นไฟล์ที่ไม่ซ้ำกันที่โฮสต์บนเว็บเซิร์ฟเวอร์ คุณอาจต้องการทำดังนี้ ใช้แอตทริบิวต์วิดีโอ 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 แล้ว:

เคล็ดลับ

หากเว็บไซต์ของคุณมีแหล่งข้อมูลวิดีโอจำนวนมากบนโดเมนเดียวกัน ขอแนะนำให้คุณตั้งค่า 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"> และใช้กับส่วนขยายแหล่งที่มาของสื่อ หากไม่คุ้นเคยกับ ด้วย 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 Worker มาดู วิธีบัฟเฟอร์วิดีโอด้วยตนเองโดยใช้ MSE ตัวอย่างด้านล่างนี้มีสมมติฐานว่า เซิร์ฟเวอร์รองรับ HTTP Range แต่นี่จะค่อนข้างคล้ายกันกับ กลุ่ม โปรดทราบว่าไลบรารีมิดเดิลแวร์บางส่วน เช่น Google's Shaka 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 รายการ เราน่าจะมีหน่วยความจำเพียงพอที่จะดึงมาแสดง ออกจากแต่ละไฟล์ แต่เราไม่ควรสร้างไฟล์ <video> ที่ซ่อนไว้ 10 ไฟล์ เอลิเมนต์และออบเจ็กต์ MediaSource 10 รายการ แล้วเริ่มป้อนข้อมูลนั้น

ตัวอย่าง 2 ส่วนด้านล่างแสดงวิธีแคชล่วงหน้าหลายกลุ่มของ วิดีโอโดยใช้ 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 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;
    });

เล่นวิดีโอ

เมื่อผู้ใช้คลิกปุ่มเล่น เราจะดึงข้อมูลส่วนแรกของวิดีโอ ที่มีอยู่ใน 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

หากคุณได้ดึงข้อมูลไฟล์วิดีโอทั้งไฟล์และบันทึกไว้ใน 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 คำขอ