Ứng dụng web tiến bộ mang đến nhiều tính năng trước đây chỉ dành cho ứ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.
Trải nghiệm phát trực tuyến nội dung nghe nhìn ngoại tuyến sẽ còn tuyệt vời hơn nữa. Đây là một tính năng nâng cao 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 đáo – tệp phương tiện có thể rất lớn. Vì vậy, bạn có thể hỏi:
- Làm cách nào để tải và lưu trữ tệp video có dung lượng 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 đã xây dựng để 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 mà không cần sử 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 phục vụ 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 phương tiện hiện có để cung cấp các tính năng này.
Trừ phi bạn có một trường hợp kinh doanh phù hợp để phát triển ứng dụng của riêng mình, việc xây dựng một PWA có tính năng truyền trực tuyến ngoại tuyến sẽ gặp phải một số 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 dùng để mang đến cho người dùng trải nghiệm nội dung nghe nhìn chất lượng cao khi không có kết nối mạng.
Tải và lưu trữ tệp phương tiện có dung lượng lớn
Ứng dụng web tiến bộ thường sử dụng API bộ nhớ đệm thuận tiện để vừa tải xuống vừa lưu trữ các thành phần cần thiết nhằm cung cấp trải nghiệm ngoại tuyến: tài liệu, bảng định kiểu, hình ảnh và các thành phần khác.
Sau đây là ví dụ cơ bản về cách sử dụng API bộ nhớ đệm trong Worker dịch vụ:
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 về mặt kỹ thuật là hoạt động, nhưng việc sử dụng API bộ nhớ đệm 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ụ: API bộ nhớ đệm 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 các 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 mọi ứng dụng video. 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 đa 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, API 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ữ các tệp đó theo từng phần bằng cách sử dụng yêu cầu phạm vi HTTP.
Giờ đây, bạn có thể đọc các phần dữ liệu bằng Fetch API, bạn cũng cần lưu trữ các phần dữ liệu đó. Có thể có một loạt siêu dữ liệu liên kết với tệp nội dung nghe nhìn của bạn, 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 phương tiện mà còn lưu trữ một đối tượng có cấu trúc, trong đó tệp phương tiệ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 phương tiện và siêu dữ liệu. Nó có thể dễ dàng lưu trữ một 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 tra cứu dữ liệu rất nhanh.
Tải tệp phương tiện xuống bằng API Tìm nạp
Chúng tôi đã xây dựng một số tính năng thú vị xoay quanh API Tìm nạp trong ứng dụng web tiến bộ (PWA) minh hoạ có tên là Kino. Mã nguồn này là công khai nên bạn có thể xem xét.
- Khả năng tạm dừng và tiếp tục tải xuống chưa hoàn tất.
- Vùng đệm tuỳ chỉnh để lưu trữ các phần dữ liệu trong cơ sở dữ liệu.
Trước khi cho thấy 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 API Tìm nạp để 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()
nằm trong một vòng lặp không? Đó là cách bạn sẽ nhận được các phần dữ liệu từ luồng có thể đọc được khi chúng đến từ mạng. Hãy xem xét mức độ hữu ích của việc 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 bị gián đoạn, các phần 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. Vì máy chủ PWA minh hoạ Kino hỗ trợ yêu cầu 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. Các 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 thích 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 bạn có thể nhận thấy rằng các hoạt động ghi IndexedDB của mình chậm hơn đáng kể so với hoạt động tải xuống. Điều này không phải do IndexedDB ghi 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 phần dữ liệu mà chúng ta nhận được 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 liên tiếp 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 vùng đệm ghi trung gian.
Khi các đoạn dữ liệu đến từ mạng, trước tiên, chúng ta sẽ thêm các đoạn dữ liệu đó vào vùng đệm. Nếu dữ liệu đến không vừa, chúng ta sẽ xả toàn bộ bộ đệm vào cơ sở dữ liệu và xoá bộ đệm đó trước khi thêm phần dữ liệu còn lại. Do đó, các hoạt động ghi IndexedDB của chúng ta ít xảy ra hơn, dẫn đến hiệu suất ghi được cải thiện đáng kể.
Phân phát tệp phương tiện từ bộ nhớ ngoại tuyến
Sau khi tải tệp phương tiện xuống, bạn có thể 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ì trong getVideoResponse()
?
Phương thức
event.respondWith()
yêu cầu đối tượngResponse
làm tham số.Hàm khởi tạo Response() cho chúng ta biết rằng có một số loại đối tượng mà chúng ta có thể sử dụng để tạo bản sao của đối tượng
Response
:Blob
,BufferSource
,ReadableStream
, v.v.Chúng ta cần một đối tượng không lưu trữ tất cả dữ liệu của đối tượng đó trong bộ nhớ, vì vậy, chúng ta có thể chọn
ReadableStream
.
Ngoài ra, vì đang xử lý các tệp lớn và muốn cho phép trình duyệt chỉ yêu cầu một phần tệp mà trình duyệt hiện cần, nên chúng ta cần triển khai một số tính năng 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 mã nguồn của trình chạy dịch vụ trong ứng dụng PWA minh hoạ Kino để tìm hiểu cách chúng ta đọc dữ liệu tệp từ IndexedDB và tạo luồng trong một ứng dụng thực tế.
Lưu ý khác
Giờ đây, khi đã giải quyết được những trở ngại chính, bạn có thể bắt đầu thêm một số tính năng hữu ích 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 bạn sẽ thấy trong PWA minh hoạ Kino:
- Tính năng 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 đa phương tiện chuyên dụng trên phần cứng hoặc từ cửa sổ bật lên thông báo nội dung nghe nhìn.
- Lưu các thành phần khác liên kết với tệp phương tiện như phụ đề và hình ảnh áp phích vào bộ nhớ đệm 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 ngoại tuyến.
Tiếp theo, bạn sẽ tìm hiểu về tính năng Phát nhanh bằng tính năng tải trước âm thanh và video.