Xuất bản: Ngày 5 tháng 7 năm 2021
Ứng dụng web tiến bộ mang đến nhiều tính năng trước đây chỉ dành cho các ứng dụng gốc trên web. Một trong những tính năng nổi bật nhất liên quan đến PWA là trải nghiệm ngoại tuyến.
Tốt hơn nữa là trải nghiệm phát trực tuyến nội dung nghe nhìn ngoại tuyến. Đây là một điểm cải tiến mà bạn có thể cung cấp cho người dùng theo một số cách. Tuy nhiên, điều này tạo ra một vấn đề thực sự đặc biệt: các tệp nội dung nghe nhìn có thể rất lớn. Vì vậy, bạn có thể thắc mắc:
- Làm cách nào để tải và lưu trữ một tệp video lớn?
- Và làm cách nào để phân phát nội dung đó cho người dùng?
Trong bài viết này, chúng ta sẽ thảo luận về câu trả lời cho những câu hỏi này, đồng thời tham khảo PWA minh hoạ Kino mà chúng tôi đã tạo. PWA này cung cấp cho bạn các ví dụ thực tế về cách triển khai trải nghiệm phát trực tuyến nội dung nghe nhìn khi không có mạng mà không cần dùng bất kỳ khung chức năng hoặc khung trình bày nào. Các ví dụ sau đây chủ yếu nhằm mục đích giáo dục, vì trong hầu hết các trường hợp, bạn nên sử dụng một trong các Khung đa phương tiện hiện có để cung cấp những tính năng này.
Trừ phi bạn có một lý do kinh doanh chính đáng để phát triển PWA của riêng mình, việc tạo một PWA có tính năng phát trực tuyến ngoại tuyến sẽ gặp phải nhiều thách thức. Trong bài viết này, bạn sẽ tìm hiểu về các API và kỹ thuật được dùng để mang đến cho người dùng trải nghiệm chất lượng cao về nội dung nghe nhìn khi không có mạng.
Tải xuống và lưu trữ một tệp nội dung nghe nhìn có kích thước lớn
Ứng dụng web tiến bộ thường sử dụng Cache API (API Bộ nhớ đệm) thuận tiện để tải xuống và lưu trữ các thành phần cần thiết nhằm mang lại trải nghiệm ngoại tuyến: tài liệu, biểu định kiểu, hình ảnh và các thành phần khác.
Sau đây là một ví dụ cơ bản về cách sử dụng Cache API trong Service Worker:
const cacheStorageName = 'v1';
this.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheStorageName).then(function(cache) {
return cache.addAll([
'index.html',
'style.css',
'scripts.js',
// Don't do this.
'very-large-video.mp4',
]);
})
);
});
Mặc dù ví dụ trên hoạt động về mặt kỹ thuật, nhưng việc sử dụng Cache API có một số hạn chế khiến việc sử dụng API này với các tệp lớn trở nên không thực tế.
Ví dụ: Cache API không:
- Cho phép bạn dễ dàng tạm dừng và tiếp tục tải xuống
- Cho phép bạn theo dõi tiến trình tải xuống
- Cung cấp cách phản hồi đúng cách đối với yêu cầu phạm vi HTTP
Tất cả những vấn đề này đều là những hạn chế khá nghiêm trọng đối với bất kỳ ứng dụng video nào. Hãy xem xét một số lựa chọn khác có thể phù hợp hơn.
Ngày nay, Fetch API là một cách trên nhiều trình duyệt để truy cập không đồng bộ vào các tệp từ xa. Trong trường hợp sử dụng của chúng ta, tính năng này cho phép bạn truy cập vào các tệp video lớn dưới dạng luồng và lưu trữ chúng tăng dần dưới dạng các đoạn bằng cách sử dụng yêu cầu phạm vi HTTP.
Giờ đây, khi có thể đọc các khối dữ liệu bằng Fetch API, bạn cũng cần lưu trữ các khối dữ liệu đó. Rất có thể tệp nội dung nghe nhìn của bạn có nhiều siêu dữ liệu được liên kết, chẳng hạn như: tên, nội dung mô tả, thời lượng chạy, danh mục, v.v.
Bạn không chỉ lưu trữ một tệp nội dung nghe nhìn mà còn lưu trữ một đối tượng có cấu trúc và tệp nội dung nghe nhìn chỉ là một trong các thuộc tính của đối tượng đó.
Trong trường hợp này, IndexedDB API cung cấp một giải pháp tuyệt vời để lưu trữ cả dữ liệu đa phương tiện và siêu dữ liệu. Nó có thể dễ dàng lưu trữ lượng lớn dữ liệu nhị phân và cũng cung cấp các chỉ mục cho phép bạn thực hiện các hoạt động tra cứu dữ liệu rất nhanh.
Tải tệp nội dung nghe nhìn xuống bằng Fetch API
Chúng tôi đã tạo một số tính năng thú vị xoay quanh Fetch API trong PWA minh hoạ của mình. Chúng tôi đặt tên cho PWA này là Kino. Mã nguồn của PWA này là mã nguồn công khai nên bạn có thể xem xét mã nguồn này.
- Khả năng tạm dừng và tiếp tục tải xuống khi chưa hoàn tất.
- Một vùng đệm tuỳ chỉnh để lưu trữ các khối dữ liệu trong cơ sở dữ liệu.
Trước khi cho biết cách triển khai các tính năng đó, trước tiên, chúng ta sẽ tóm tắt nhanh cách bạn có thể sử dụng Fetch API để tải tệp xuống.
/**
* Downloads a single file.
*
* @param {string} url URL of the file to be downloaded.
*/
async function downloadFile(url) {
const response = await fetch(url);
const reader = response.body.getReader();
do {
const { done, dataChunk } = await reader.read();
// Store the `dataChunk` to IndexedDB.
} while (!done);
}
Bạn có nhận thấy await reader.read()
đang ở trong một vòng lặp không? Đó là cách bạn sẽ nhận được các khối dữ liệu từ một luồng có thể đọc khi chúng đến từ mạng. Hãy xem xét mức độ hữu ích của tính năng này: bạn có thể bắt đầu xử lý dữ liệu ngay cả trước khi tất cả dữ liệu đến từ mạng.
Tiếp tục tải xuống
Khi quá trình tải xuống bị tạm dừng hoặc gián đoạn, các khối dữ liệu đã đến sẽ được lưu trữ an toàn trong cơ sở dữ liệu IndexedDB. Sau đó, bạn có thể hiển thị một nút để tiếp tục tải xuống trong ứng dụng của mình. Vì máy chủ PWA của bản minh hoạ Kino hỗ trợ yêu cầu về phạm vi HTTP nên việc tiếp tục tải xuống khá đơn giản:
async downloadFile() {
// this.currentFileMeta contains data from IndexedDB.
const { bytesDownloaded, url, downloadUrl } = this.currentFileMeta;
const fetchOpts = {};
// If we already have some data downloaded,
// request everything from that position on.
if (bytesDownloaded) {
fetchOpts.headers = {
Range: `bytes=${bytesDownloaded}-`,
};
}
const response = await fetch(downloadUrl, fetchOpts);
const reader = response.body.getReader();
let dataChunk;
do {
dataChunk = await reader.read();
if (!dataChunk.done) this.buffer.add(dataChunk.value);
} while (!dataChunk.done && !this.paused);
}
Vùng đệm ghi tuỳ chỉnh cho IndexedDB
Trên lý thuyết, quy trình ghi các giá trị dataChunk
vào cơ sở dữ liệu IndexedDB rất đơn giản. Những giá trị đó đã là các thực thể ArrayBuffer
, có thể lưu trữ trực tiếp trong IndexedDB, vì vậy, chúng ta chỉ cần tạo một đối tượng có hình dạng phù hợp và lưu trữ đối tượng đó.
const dataItem = {
url: fileUrl,
rangeStart: dataStartByte,
rangeEnd: dataEndByte,
data: dataChunk,
}
// Name of the store that will hold your data.
const storeName = 'fileChunksStorage'
// `db` is an instance of `IDBDatabase`.
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const putRequest = store.put(data);
putRequest.onsuccess = () => { ... }
Mặc dù phương pháp này có hiệu quả, nhưng có thể bạn sẽ nhận thấy các thao tác ghi IndexedDB chậm hơn đáng kể so với thao tác tải xuống. Điều này không phải do các thao tác ghi IndexedDB diễn ra chậm, mà là do chúng ta đang thêm nhiều chi phí giao dịch bằng cách tạo một giao dịch mới cho mỗi khối dữ liệu mà chúng ta nhận được từ một mạng.
Các đoạn đã tải xuống có thể khá nhỏ và có thể được phát ra từ luồng một cách nhanh chóng. Bạn cần giới hạn tốc độ ghi IndexedDB. Trong PWA minh hoạ Kino, chúng ta thực hiện việc này bằng cách triển khai một vùng đệm ghi trung gian.
Khi các khối dữ liệu đến từ mạng, trước tiên, chúng ta sẽ thêm các khối đó vào vùng đệm. Nếu dữ liệu đến không phù hợp, chúng ta sẽ xoá toàn bộ vùng đệm vào cơ sở dữ liệu và xoá vùng đệm đó trước khi thêm phần còn lại của dữ liệu. Do đó, các thao tác ghi IndexedDB của chúng tôi ít thường xuyên hơn, giúp cải thiện đáng kể hiệu suất ghi.
Phân phát tệp nội dung nghe nhìn từ bộ nhớ ngoại tuyến
Sau khi tải một tệp đa phương tiện xuống, có thể bạn muốn trình chạy dịch vụ phân phát tệp đó từ IndexedDB thay vì tìm nạp tệp từ mạng.
/**
* The main service worker fetch handler.
*
* @param {FetchEvent} event Fetch event.
*/
const fetchHandler = async (event) => {
const getResponse = async () => {
// Omitted Cache API code used to serve static assets.
const videoResponse = await getVideoResponse(event);
if (videoResponse) return videoResponse;
// Fallback to network.
return fetch(event.request);
};
event.respondWith(getResponse());
};
self.addEventListener('fetch', fetchHandler);
Vậy bạn cần làm gì ở getVideoResponse()
?
Phương thức
event.respondWith()
yêu cầu một đối tượngResponse
làm tham số.Hàm dựng Response() cho chúng ta biết rằng có một số loại đối tượng mà chúng ta có thể dùng để khởi tạo một đối tượng
Response
:Blob
,BufferSource
,ReadableStream
và nhiều đối tượng khác.Chúng ta cần một đối tượng không lưu giữ tất cả dữ liệu trong bộ nhớ, vì vậy, có lẽ chúng ta sẽ muốn chọn
ReadableStream
.
Ngoài ra, vì chúng tôi đang xử lý các tệp lớn và muốn cho phép trình duyệt chỉ yêu cầu phần tệp mà chúng hiện cần, nên chúng tôi cần triển khai một số hỗ trợ cơ bản cho yêu cầu phạm vi HTTP.
/**
* Respond to a request to fetch offline video file and construct a response
* stream.
*
* Includes support for `Range` requests.
*
* @param {Request} request Request object.
* @param {Object} fileMeta File meta object.
*
* @returns {Response} Response object.
*/
const getVideoResponse = (request, fileMeta) => {
const rangeRequest = request.headers.get('range') || '';
const byteRanges = rangeRequest.match(/bytes=(?<from>[0-9]+)?-(?<to>[0-9]+)?/);
// Using the optional chaining here to access properties of
// possibly nullish objects.
const rangeFrom = Number(byteRanges?.groups?.from || 0);
const rangeTo = Number(byteRanges?.groups?.to || fileMeta.bytesTotal - 1);
// Omitting implementation for brevity.
const streamSource = {
pull(controller) {
// Read file data here and call `controller.enqueue`
// with every retrieved chunk, then `controller.close`
// once all data is read.
}
}
const stream = new ReadableStream(streamSource);
// Make sure to set proper headers when supporting range requests.
const responseOpts = {
status: rangeRequest ? 206 : 200,
statusText: rangeRequest ? 'Partial Content' : 'OK',
headers: {
'Accept-Ranges': 'bytes',
'Content-Length': rangeTo - rangeFrom + 1,
},
};
if (rangeRequest) {
responseOpts.headers['Content-Range'] = `bytes ${rangeFrom}-${rangeTo}/${fileMeta.bytesTotal}`;
}
const response = new Response(stream, responseOpts);
return response;
Bạn có thể xem Kino PWA minh hoạ mã nguồn trình chạy dịch vụ để tìm hiểu cách chúng tôi đọc dữ liệu tệp từ IndexedDB và tạo một luồng trong một ứng dụng thực.
Lưu ý khác
Khi đã vượt qua những trở ngại chính, giờ đây, bạn có thể bắt đầu thêm một số tính năng nên có vào ứng dụng video của mình. Sau đây là một số ví dụ về các tính năng mà bạn sẽ thấy trong PWA minh hoạ Kino:
- Tích hợp Media Session API cho phép người dùng kiểm soát việc phát nội dung nghe nhìn bằng các phím nội dung nghe nhìn chuyên dụng trên thiết bị hoặc từ cửa sổ thông báo bật lên về nội dung nghe nhìn.
- Lưu vào bộ nhớ đệm các tài sản khác liên kết với tệp đa phương tiện, chẳng hạn như phụ đề và hình ảnh áp phích bằng Cache API cũ.
- Hỗ trợ tải luồng video (DASH, HLS) xuống trong ứng dụng. Vì tệp kê khai luồng thường khai báo nhiều nguồn có tốc độ bit khác nhau, nên bạn cần chuyển đổi tệp kê khai và chỉ tải một phiên bản nội dung nghe nhìn xuống trước khi lưu trữ để xem khi không có mạng.
Tiếp theo, bạn sẽ tìm hiểu về Tính năng phát nhanh có tải trước âm thanh và video.