Phát nhanh với tính năng tải trước âm thanh và video

Cách tăng tốc độ phát nội dung đa phương tiện bằng cách chủ động tải trước tài nguyên.

François Beaufort
François Beaufort

Thời gian bắt đầu phát nhanh hơn có nghĩa là nhiều người xem video của bạn hoặc nghe âm thanh. Đó là một sự thật đã biết. Trong bài viết này, tôi sẽ tìm hiểu những kỹ thuật bạn có thể sử dụng để tăng tốc độ phát lại âm thanh và video bằng cách chủ động tải trước tài nguyên tuỳ theo trường hợp sử dụng của bạn.

Nhà cung cấp: Bmate Foundation bản quyền | www.blender.org .

Tôi sẽ mô tả 3 phương pháp tải trước tệp đa phương tiện, bắt đầu từ những ưu điểm của chúng và nhược điểm

Rất tốt... Nhưng...
Thuộc tính tải trước video Sử dụng đơn giản cho một tệp duy nhất được lưu trữ trên máy chủ web. Trình duyệt có thể hoàn toàn bỏ qua thuộc tính này.
Quá trình tìm nạp tài nguyên bắt đầu khi tài liệu HTML đã được tải hoàn toàn và đã phân tích cú pháp.
Tiện ích nguồn phương tiện (MSE) bỏ qua thuộc tính preload trên các phần tử nội dung nghe nhìn vì ứng dụng này chịu trách nhiệm về cung cấp phương tiện cho MSE.
Tải trước đường liên kết Buộc trình duyệt yêu cầu tài nguyên video mà không chặn sự kiện onload của tài liệu. Các yêu cầu Phạm vi HTTP không tương thích.
Tương thích với MSE và các phân đoạn tệp. Chỉ nên sử dụng cho các tệp đa phương tiện nhỏ (<5 MB) khi tìm nạp đầy đủ tài nguyên.
Lưu theo cách thủ công vào bộ nhớ đệm Toàn quyền kiểm soát Trang web chịu trách nhiệm xử lý lỗi phức tạp.

Thuộc tính tải trước video

Nếu nguồn video là một tệp duy nhất được lưu trữ trên máy chủ web, bạn có thể muốn sử dụng thuộc tính video preload để cung cấp gợi ý cho trình duyệt về cách cần tải trước nhiều thông tin hoặc nội dung. Điều này có nghĩa là Tiện ích nguồn phương tiện (MSE) không tương thích với preload.

Quá trình tìm nạp tài nguyên sẽ chỉ bắt đầu khi tài liệu HTML ban đầu đã được được tải và phân tích cú pháp hoàn toàn (ví dụ: sự kiện DOMContentLoaded đã kích hoạt) trong khi sự kiện load rất khác sẽ được kích hoạt khi tài nguyên thực sự đã được tìm nạp.

Việc đặt thuộc tính preload thành metadata cho biết rằng người dùng không dự kiến sẽ cần video, nhưng nó tìm nạp siêu dữ liệu của nó (kích thước, theo dõi danh sách, thời lượng, v.v.) đều được mong muốn. Lưu ý rằng bắt đầu trong Chrome 64, thì giá trị mặc định của preloadmetadata. (Bây giờ là auto trước đó).

<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>

Việc đặt thuộc tính preload thành auto cho biết trình duyệt có thể lưu vào bộ nhớ đệm đủ dữ liệu để có thể phát hoàn tất mà không yêu cầu dừng lại lưu vào bộ đệm thêm.

<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>

Tuy nhiên, có một số điểm cần lưu ý. Do đây chỉ là một gợi ý, trình duyệt có thể hoàn toàn bỏ qua thuộc tính preload. Dưới đây là một số quy tắc tại thời điểm viết bài đã áp dụng trong Chrome:

  • Khi Trình tiết kiệm dữ liệu được bật, Chrome sẽ buộc giá trị preload thành none.
  • Trong Android 4.3, Chrome buộc giá trị preload thành none do một Android Lỗi.
  • Trên kết nối di động (2G, 3G và 4G), Chrome buộc giá trị preload thành metadata

Mẹo

Nếu trang web của bạn chứa nhiều tài nguyên video trên cùng một miền, bạn nên đặt giá trị preload thành metadata hoặc xác định poster và đặt preload thành none. Bằng cách đó, bạn sẽ tránh được số lượng kết nối HTTP tối đa đến cùng một miền (6 kết nối theo HTTP 1.1) có thể khiến tài nguyên bị treo. Xin lưu ý rằng việc này cũng có thể cải thiện tốc độ trang nếu video không phải là một phần trải nghiệm người dùng cốt lõi.

Như đã đề cập trong các bài viết khác, tải trước đường liên kết là một phương thức tìm nạp khai báo cho phép bạn buộc trình duyệt thực hiện yêu cầu về tài nguyên mà không chặn sự kiện load và trong khi tải trang xuống. Tài liệu tham khảo được tải qua <link rel="preload"> được lưu trữ cục bộ trong trình duyệt và trơ hiệu quả cho đến khi chúng được tham chiếu rõ ràng trong DOM, JavaScript, hoặc CSS.

Hoạt động tải trước khác với hoạt động tìm nạp trước ở chỗ hoạt động này tập trung vào hoạt động điều hướng hiện tại và tìm nạp tài nguyên theo mức độ ưu tiên dựa trên loại của chúng (tập lệnh, kiểu, phông chữ, video, âm thanh, v.v.). Bạn nên dùng trình duyệt này để khởi động bộ nhớ đệm của trình duyệt cho phiên hoạt động.

Tải trước toàn bộ video

Sau đây là cách tải trước video đầy đủ trên trang web của bạn để khi JavaScript yêu cầu tìm nạp nội dung video, nó được đọc từ bộ nhớ đệm dưới dạng tài nguyên có thể đã được trình duyệt lưu vào bộ nhớ đệm. Nếu yêu cầu tải trước không đã kết thúc, thì quá trình tìm nạp mạng thông thường sẽ diễn ra.

<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>

Vì tài nguyên được tải trước sẽ được một phần tử video sử dụng trong ví dụ: giá trị đường liên kết tải trước asvideo. Nếu đó là âm thanh thì đó sẽ là as="audio".

Tải trước phân đoạn đầu tiên

Ví dụ bên dưới cho biết cách tải trước đoạn đầu tiên của video bằng <link rel="preload"> và sử dụng đoạn này với Phần mở rộng về nguồn nội dung nghe nhìn. Nếu bạn không quen thuộc với API JavaScript MSE, hãy xem Kiến thức cơ bản về MSE.

Để cho đơn giản, giả sử toàn bộ video đã được chia thành các tệp nhỏ hơn như file_1.webm, file_2.webm, file_3.webm, v.v.

<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>

Hỗ trợ

Bạn có thể phát hiện sự hỗ trợ của nhiều loại as cho <link rel=preload> bằng phương thức đoạn mã dưới đây:

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

Lưu thủ công vào bộ đệm

Trước khi tìm hiểu sâu hơn về API Bộ nhớ đệm và trình chạy dịch vụ, hãy xem cách lưu video vào bộ đệm thủ công bằng MSE. Ví dụ bên dưới giả định rằng trang web của bạn máy chủ hỗ trợ HTTP Range nhưng cách này sẽ khá giống với tệp phân khúc. Lưu ý rằng một số thư viện phần mềm trung gian như Sshaka của Google Trình phát, Trình phát JWVideo.js là được thiết kế để xử lý vấn đề này cho bạn.

<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>

Những yếu tố nên cân nhắc

Do bạn hiện đang kiểm soát toàn bộ trải nghiệm lưu nội dung nghe nhìn vào bộ đệm, nên tôi khuyên bạn hãy xem xét mức pin của thiết bị, "Chế độ tiết kiệm dữ liệu" tùy chọn người dùng và khi xem xét tải trước.

Nhận biết về pin

Tính đến mức pin của người dùng thiết bị trước khi suy nghĩ về cách tải trước một video. Chế độ này sẽ duy trì thời lượng pin khi mức pin thấp.

Tắt tính năng tải trước hoặc ít nhất là tải trước video có độ phân giải thấp hơn khi thiết bị sắp hết pin.

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

Phát hiện "Trình tiết kiệm dữ liệu"

Dùng tiêu đề yêu cầu gợi ý ứng dụng Save-Data để mang lại giao diện nhanh và nhẹ các ứng dụng cho người dùng đã chọn tham gia "tiết kiệm dữ liệu" trong trình duyệt. Bằng cách xác định tiêu đề yêu cầu này, ứng dụng của bạn có thể tuỳ chỉnh và mang lại trải nghiệm người dùng được tối ưu hoá cho các yếu tố bị hạn chế về chi phí và hiệu suất người dùng.

Xem Phân phối các ứng dụng nhanh và nhẹ bằng dữ liệu tiết kiệm để tìm hiểu thêm.

Tải thông minh dựa trên thông tin mạng

Bạn nên kiểm tra navigator.connection.type trước khi tải trước. Thời gian nếu bạn đặt thành cellular, thì bạn có thể ngăn tải trước và khuyên người dùng nhà cung cấp dịch vụ mạng di động của họ có thể đang tính phí băng thông và chỉ bắt đầu tự động phát nội dung đã lưu vào bộ nhớ đệm trước đó.

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

Xem mẫu Thông tin mạng để tìm hiểu cách phản ứng với mạng những thay đổi nhỏ.

Lưu trước nhiều phân đoạn đầu tiên vào bộ nhớ đệm

Bây giờ nếu tôi muốn tải trước một số nội dung đa phương tiện theo suy đoán mà không biết cuối cùng người dùng sẽ chọn nội dung đa phương tiện nào? Nếu người dùng đang sử dụng trang web chứa 10 video, chúng tôi có thể có đủ bộ nhớ để tìm nạp một video tệp phân đoạn khỏi mỗi tệp, nhưng chắc chắn chúng ta không nên tạo 10 tệp <video> ẩn và 10 đối tượng MediaSource rồi bắt đầu cấp dữ liệu đó.

Ví dụ gồm hai phần dưới đây cho bạn biết cách lưu trước vào bộ nhớ đệm nhiều phân đoạn đầu tiên của bằng cách dùng API Bộ nhớ đệm mạnh mẽ và dễ sử dụng. Lưu ý rằng tên tương tự cũng có thể đạt được nhờ IndexedDB. Chúng tôi chưa sử dụng trình chạy dịch vụ dưới dạng Bạn cũng có thể truy cập vào Cache API qua đối tượng window.

Tìm nạp và lưu vào bộ nhớ đệm

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

Lưu ý rằng nếu sử dụng các yêu cầu HTTP Range, tôi sẽ phải tạo lại theo cách thủ công đối tượng Response làm API Bộ nhớ đệm chưa hỗ trợ phản hồi Range. Hãy xin lưu ý rằng việc gọi networkResponse.arrayBuffer() sẽ tìm nạp toàn bộ nội dung của phản hồi cùng một lúc vào bộ nhớ kết xuất. Đó là lý do bạn nên sử dụng phạm vi nhỏ.

Để tham khảo, tôi đã sửa đổi một phần của ví dụ ở trên để lưu Phạm vi HTTP yêu cầu vào bộ nhớ đệm trước của video.

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

Phát video

Khi người dùng nhấp vào nút phát, chúng tôi sẽ tìm nạp đoạn video đầu tiên có sẵn trong API Bộ nhớ đệm để quá trình phát có thể bắt đầu ngay lập tức nếu có. Nếu không, chúng tôi sẽ chỉ tìm nạp mã từ mạng. Xin lưu ý rằng các trình duyệt và người dùng có thể quyết định xoá Bộ nhớ đệm.

Như đã thấy trước đó, chúng ta sử dụng MSE để đưa đoạn đầu tiên của video lên video .

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

Tạo phản hồi của Dải ô bằng một trình chạy dịch vụ

Giờ nếu bạn đã tìm nạp toàn bộ tệp video và lưu tệp đó vào API Bộ nhớ đệm? Khi trình duyệt gửi một yêu cầu HTTP Range, chắc chắn bạn sẽ không muốn đưa toàn bộ video vào bộ nhớ kết xuất vì Cache API không chưa hỗ trợ Range phản hồi.

Hãy để tôi chỉ cho bạn cách chặn các yêu cầu này và trả về một Range tuỳ chỉnh phản hồi của một trình chạy dịch vụ.

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

Điều quan trọng cần lưu ý là tôi đã sử dụng response.blob() để tạo lại lát cắt này vì việc này chỉ giúp tôi xử lý tệp trong khi response.arrayBuffer() đưa toàn bộ tệp vào bộ nhớ kết xuất.

Tiêu đề HTTP X-From-Cache tuỳ chỉnh của tôi có thể dùng để biết liệu yêu cầu này có từ bộ nhớ đệm hoặc từ mạng. Người chơi có thể sử dụng thông tin này như ShakaPlayer để bỏ qua thời gian phản hồi dưới dạng chỉ báo về tốc độ mạng.

Hãy xem Ứng dụng đa phương tiện mẫu chính thức và cụ thể là Tệp ranged-response.js để có giải pháp hoàn chỉnh về cách xử lý Range yêu cầu.