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.
Việc bắt đầu phát nhanh hơn sẽ giúp nhiều người xem video hoặc nghe âm thanh của bạn hơn. Đó là một sự thật đã biết. Trong bài viết này, tôi sẽ khám phá các kỹ thuật mà bạn có thể dùng để tăng tốc độ phát âm thanh và video bằng cách chủ động tải trước các tài nguyên tuỳ theo trường hợp sử dụng của bạn.
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ừ ưu và nhược điểm của những phương pháp đó.
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 nghe nhì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 nghe nhì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.
|
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, thì bạn nên sử dụng thuộc tính video preload
để 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. Tức 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 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 dự kiến sẽ không cần video, nhưng việc tìm nạp siêu dữ liệu của video (kích thước, danh sách theo dõi, thời lượng, v.v.) là mong muốn. Xin lưu ý rằng bắt đầu 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 đủ dữ liệu vào bộ nhớ đệm để 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ố điểm cần 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 một 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
rồi đặ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 đến cùng một miền (6 theo thông số kỹ thuật 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 bạn không cung cấp video không phải trong trải nghiệm người dùng cốt lõi.
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à hoạt động một cách 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 có 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 thủ công vào bộ đệm
Trước khi tìm hiểu sâu 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 máy chủ web của bạn hỗ trợ các yêu cầu HTTP Range
nhưng điều này khá giống với 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ư shaka Player của Google, JW Player và Video.js được xây dựng để xử lý việc 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
Giờ đây, vì bạn là người 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 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 là "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 nghĩ đến việc tải trước video. Điều này sẽ giúp duy trì thời lượng pin khi mức năng lượng 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"
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 tham gia chế độ "tiết kiệm dữ liệu" trong trình duyệt. Bằng cách xác định tiêu đề của yêu cầu này, ứng dụng của bạn có thể tuỳ chỉnh và cung cấp trải nghiệm người dùng được tối ưu hoá cho người dùng bị hạn chế về chi phí và hiệu suất.
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. Khi đặt thuộc tính này thành cellular
, thì bạn có thể ngăn việc tải trước và thông báo cho người dùng rằ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.
}
}
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 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 nào? Nếu người dùng đang truy cập một trang web chứa 10 video, chúng ta có thể có đủ bộ nhớ để tìm nạp 1 tệp phân đoạn từ mỗi video. Tuy nhiên, chúng ta chắc chắn không nên tạo 10 phần tử <video>
ẩn và 10 đối tượng MediaSource
và bắt đầu cung cấp dữ liệu đó.
Ví dụ 2 phần bên dưới cho bạn biết cách lưu trước nhiều phân đoạn đầu tiên của video vào bộ nhớ đệm bằng cách sử dụng API Bộ nhớ đệm 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 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 để 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ụ
Nếu bạn đã tìm nạp toàn bộ một tệp video và lưu 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ớ của 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 chỉ cho bạ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 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 phản hồi được cắt lát này vì việc này chỉ đơn giản là cho 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 yêu cầu này đến từ bộ nhớ đệm hay từ mạng. Trình phát này có thể sử dụng trình phát này như ShakaPlayer để bỏ qua thời gian phản hồi. Đây là 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 để biết giải pháp hoàn chỉnh về cách xử lý các yêu cầu Range
.