Cách tăng tốc phát nội dung nghe nhìn bằng cách chủ động tải trước tài nguyên.
Thời gian bắt đầu phát nhanh hơn đồng nghĩa với việc nhiều người sẽ xem video hoặc nghe âm thanh của bạn hơn. Đó là một thực tế đã biết. Trong bài viết này, tôi sẽ khám phá các kỹ thuật bạn có thể sử dụng để tăng tốc phát âm thanh và video bằng cách chủ động tải trước tài nguyên tuỳ thuộc vào trường hợp sử dụng của bạn.
Tôi sẽ mô tả ba phương thức tải trước tệp phương tiện, bắt đầu bằng ưu và khuyết điểm của các phương thức đó.
Thật tuyệt... | Nhưng... | |
---|---|---|
Thuộc tính tải trước video | Dễ sử dụng cho một tệp duy nhất được lưu trữ trên máy chủ web. | Trình duyệt có thể bỏ qua hoàn toàn 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 và phân tích cú pháp hoàn toàn. | ||
Tiện ích nguồn nội dung đa phương tiện (MSE) bỏ qua thuộc tính preload trên các phần tử nội dung đa phương tiện vì ứng dụng chịu trách nhiệm cung cấp nội dung đa phương tiện cho MSE.
|
||
Tải trước đường liên kết |
Buộc trình duyệt đưa ra yêu cầu về tài nguyên video mà không chặn sự kiện onload của tài liệu.
|
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 dùng cho các tệp phương tiện nhỏ (<5 MB) khi tìm nạp toàn bộ tài nguyên. | |
Lưu vào bộ đệm theo cách thủ công | Toàn quyền kiểm soát | Việc xử lý lỗi phức tạp là trách nhiệm của trang web. |
Thuộc tính tải trước video
Nếu nguồn video là một tệp riêng biệt được lưu trữ trên máy chủ web, bạn nên sử dụng thuộc tính preload
của video để cung cấp gợi ý cho trình duyệt về lượng thông tin hoặc nội dung cần tải trước. Điều này có nghĩa là Tiện ích nguồn nội dung đa 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 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 cần video, nhưng bạn nên tìm nạp siêu dữ liệu của video (kích thước, danh sách bản nhạc, thời lượng, v.v.). Xin lưu ý rằng kể từ Chrome
64, giá trị mặc định của preload
là metadata
. (Trước đây là 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>
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 cần dừng để 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ố lưu ý. Vì đây chỉ là gợi ý nên trình duyệt có thể hoàn toàn bỏ qua thuộc tính preload
. Tại thời điểm viết bài, sau đây là một số quy tắc được áp dụng trong Chrome:
- Khi bạn bật Trình tiết kiệm dữ liệu, Chrome sẽ buộc giá trị
preload
thànhnone
. - Trong Android 4.3, Chrome buộc giá trị
preload
thànhnone
do Lỗi Android. - Trên kết nối di động (2G, 3G và 4G), Chrome buộc giá trị
preload
thànhmetadata
.
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 thuộc tính poster
và đặt preload
thành none
. Bằng cách đó, bạn sẽ tránh đạt đến số lượng kết nối HTTP tối đa với cùng một miền (6 theo thông số kỹ thuật HTTP 1.1) có thể làm treo quá trình tải tài nguyên. 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 của trải nghiệm cốt lõi dành cho người dùng.
Tải trước đường liên kết
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 đưa ra yêu cầu về một tài nguyên mà không chặn sự kiện load
và trong khi trang đang tải xuống. Các tài nguyên được tải qua <link rel="preload">
được lưu trữ cục bộ trong trình duyệt và không hoạt động cho đến khi được tham chiếu rõ ràng trong DOM, JavaScript hoặc CSS.
Tải trước khác với tải trước ở chỗ tính nă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 tài nguyên (tập lệnh, kiểu, phông chữ, video, âm thanh, v.v.). Bạn nên sử dụng tính năng này để khởi động bộ nhớ đệm của trình duyệt cho các phiên hiện tại.
Tải trước toàn bộ video
Sau đây là cách tải trước toàn bộ video trên trang web của bạn để khi JavaScript yêu cầu tìm nạp nội dung video, nội dung đó sẽ được đọc từ bộ nhớ đệm vì trình duyệt có thể đã lưu tài nguyên vào bộ nhớ đệm. Nếu yêu cầu tải trước chưa hoàn tất, thì một lượt 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ì phần tử video trong ví dụ sẽ sử dụng tài nguyên được tải trước, nên giá trị đường liên kết tải trước as
là video
. Nếu là phần tử âm thanh, thì giá trị này sẽ là as="audio"
.
Tải trước phân đoạn đầu tiên
Ví dụ dưới đây cho thấy cách tải trước phân đoạn đầu tiên của video bằng <link
rel="preload">
và sử dụng phân đoạn đó với Tiện ích nguồn nội dung nghe nhìn. Nếu bạn chưa quen với API JavaScript MSE, hãy xem phần Kiến thức cơ bản về MSE.
Để đơn giản, hãy 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 khả năng hỗ trợ nhiều loại as
cho <link rel=preload>
bằng cá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 vào bộ đệm theo cách thủ công
Trước khi tìm hiểu về Cache API và worker dịch vụ, hãy xem cách lưu video vào bộ đệm theo cách thủ công bằng MSE. Ví dụ dưới đây giả định rằng máy chủ web của bạn hỗ trợ các yêu cầu Range
HTTP, nhưng điều này cũng tương tự như các phân đoạn tệp. Xin lưu ý rằng một số thư viện phần mềm trung gian như Trình phát Shaka của Google, JW Player và Video.js được tạo để 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
Vì bạn hiện đang kiểm soát toàn bộ trải nghiệm lưu vào bộ đệm nội dung nghe nhìn, nên bạn nên cân nhắc mức pin của thiết bị, lựa chọn ưu tiên của người dùng về "Chế độ tiết kiệm dữ liệu" và thông tin mạng khi cân nhắc việc tải trước.
Nhận biết pin
Hãy tính đến mức pin của thiết bị của người dùng trước khi cân nhắc tải trước video. Điều này sẽ giúp duy trì thời lượng pin khi mức pin yếu.
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"
Sử dụng tiêu đề yêu cầu gợi ý ứng dụng Save-Data
để phân phối các ứng dụng nhanh và nhẹ cho những người dùng đã chọn sử dụng chế độ "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 những người dùng bị hạn chế về chi phí và hiệu suất.
Hãy xem phần Phân phối ứng dụng nhanh và nhẹ bằng tính năng Lưu dữ liệu để 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. Khi giá trị này được đặt thành cellular
, bạn có thể ngăn tải trước và thông báo cho người dùng rằng nhà mạng di động của họ có thể đang tính phí băng thông và chỉ bắt đầu phát tự động 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.
}
}
Hãy xem mẫu Thông tin mạng để tìm hiểu cách phản ứng với các thay đổi về mạng.
Lưu nhiều phân khúc đầu tiên vào bộ nhớ đệm trước
Bây giờ, nếu tôi muốn tải trước một số nội dung đa phương tiện mà không biết người dùng sẽ chọn nội dung đa phương tiện nào thì sao? Nếu người dùng đang ở trên một trang web chứa 10 video, thì có thể chúng ta có đủ bộ nhớ để tìm nạp một tệp phân đoạn từ mỗi video, nhưng chúng ta tuyệt đối không được tạo 10 phần tử <video>
ẩn và 10 đối tượng MediaSource
rồi bắt đầu cung cấp dữ liệu đó.
Ví dụ gồm hai phần dưới đây cho bạn biết cách lưu nhiều phân đoạn đầu tiên của video vào bộ nhớ đệm trước bằng Cache API mạnh mẽ và dễ sử dụng. Xin lưu ý rằng bạn cũng có thể thực hiện một số thao tác tương tự bằng IndexedDB. Chúng ta chưa sử dụng trình chạy dịch vụ vì bạn cũng có thể truy cập vào API bộ nhớ đệm từ đố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;
});
});
}
Xin lưu ý rằng nếu muốn sử dụng các yêu cầu Range
HTTP, tôi sẽ phải tạo lại đối tượng Response
theo cách thủ công vì API bộ nhớ đệm chưa hỗ trợ các phản hồi Range
. 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ớ trình kết xuất. Đó là lý do bạn nên sử dụng các dải nhỏ.
Để tham khảo, tôi đã sửa đổi một phần của ví dụ ở trên để lưu các yêu cầu Phạm vi HTTP 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 ta sẽ tìm nạp phân đoạn video đầu tiên có trong API bộ nhớ đệm để bắt đầu phát ngay nếu có. Nếu không, chúng ta sẽ chỉ tìm nạp từ mạng. Xin lưu ý rằng 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 để truyền đoạn video đầu tiên đó đến phần tử 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 Phạm vi bằng worker dịch vụ
Bây 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 thì sao? Khi trình duyệt gửi yêu cầu Range
HTTP, chắc chắn bạn không muốn đưa toàn bộ video vào bộ nhớ trình kết xuất vì API bộ nhớ đệm chưa hỗ trợ phản hồi Range
.
Vì vậy, hãy để tôi hướng dẫn cách chặn các yêu cầu này và trả về phản hồi Range
tuỳ chỉnh từ một worker 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 phản hồi được cắt này vì thao tác này chỉ cung cấp cho tôi một tay cầm cho tệp trong khi response.arrayBuffer()
đưa toàn bộ tệp vào bộ nhớ trình kết xuất.
Bạn có thể sử dụng tiêu đề HTTP X-From-Cache
tuỳ chỉnh của tôi để biết yêu cầu này đến từ bộ nhớ đệm hay từ mạng. Trình phát (chẳng hạn như ShakaPlayer) có thể sử dụng chỉ số này để bỏ qua thời gian phản hồi dưới dạng chỉ báo 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 để biết giải pháp đầy đủ về cách xử lý các yêu cầu Range
.