PWA có phát trực tuyến ngoại tuyến

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

Ứng dụng web tiến bộ mang đến nhiều tính năng dành riêng cho quảng cáo gốc trước đây ứng dụng lên web. Một trong những tính năng nổi bật nhất gắn với 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, nâng cao mà bạn có thể cung cấp cho người dùng của mình theo một số cách khác nhau. Tuy nhiên, điều này tạo ra một vấn đề thực sự có một không hai — các tệp nội dung nghe nhìn có thể có kích thước rất lớn. Loại đối thủ sau lượt đánh bóng có thể bạn đang hỏi:

  • Làm cách nào để tải xuống và lưu trữ tệp video có kích thước lớn?
  • Và làm cách nào để phân phối quảng cáo đó đến người dùng?

Trong bài viết này, chúng tôi sẽ thảo luận câu trả lời cho những câu hỏi nêu trên, đồng thời tham khảo PWA Kino minh hoạ mà chúng tôi tạo ra để cung cấp cho bạn các ví dụ về cách bạn có thể triển khai trải nghiệm phát trực tuyến nội dung nghe nhìn ngoại tuyến mà không bằng bất kỳ khung chức năng hoặc trình bày nào. Sau đây là các ví dụ chủ yếu cho mục đích giáo dục vì trong hầu hết trường hợp, có lẽ bạn nên sử dụng một trong các Khung phương tiện hiện có để cung cấp những tính năng này.

Tạo một ứng dụng web tiến bộ (PWA) 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 với phát trực tiếp ngoại tuyến có những thách thức riêng. Trong bài viết này, bạn sẽ tìm hiểu về các API và kỹ thuật dùng để cung cấp cho người dùng nội dung nghe nhìn ngoại tuyến chất lượng cao của bạn.

Tải xuống và lưu trữ tệp đa phương tiện có kích thước lớn

Các ứng dụng web tiến bộ thường sử dụng API Bộ nhớ đệm thuận tiện cho cả hai ứng dụng tải xuống và lưu trữ các tài sản cần thiết để cung cấp trải nghiệm ngoại tuyến: tài liệu, biểu định kiểu, hình ảnh và các mục khác.

Dưới đây là ví dụ cơ bản về cách sử dụng API Bộ nhớ đệm trong Trình chạy 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 hoạt động về mặt kỹ thuật, nhưng việc sử dụng API Cache cũng phải khiến việc sử dụng với các tệp lớn không thực tế.

Ví dụ: API Bộ nhớ đệm không:

  • Giú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
  • Đưa ra cách phản hồi đúng cách đối với các yêu cầu về phạm vi HTTP

Tất cả những vấn đề này 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ố phương án khác có thể phù hợp hơn.

Ngày nay, API Tìm nạp là một cách trên nhiều trình duyệt để truy cập từ xa tệp. Trong trường hợp sử dụng của chúng tôi, tính năng này cho phép bạn truy cập 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 phân đ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 phần dữ liệu bằng API Tìm nạp, bạn cũng cần lưu trữ chúng. Có thể có một loạt siêu dữ liệu được liên kết với nội dung đa phương tiện của bạn tệp như: tên, mô tả, thời lượng thời gian chạy, danh mục, v.v.

Bạn không chỉ lưu trữ một tệp đa phương tiện, mà bạn đang lưu trữ đối tượng có cấu trúc, và tệp đa phương tiện chỉ là một trong các thuộc tính của nó.

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 tra cứu dữ liệu rất nhanh.

Tải tệp đa 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ị liên quan đến API Tìm nạp trong PWA minh hoạ, được chúng tôi đặt tên là Kinomã nguồn là công khai, nên bạn có thể xem lại mã này.

  • Có thể tạm dừng và tiếp tục quá trình 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 trình bày cách triển khai những tính năng đó, đầu tiên chúng tôi sẽ tóm tắt nhanh về 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 nhận thấy await reader.read() có trong vòng lặp không? Đây là cách bạn sẽ nhận được các phần của dữ liệu từ một luồng có thể đọc được khi chúng đến từ mạng. Cân nhắc cách thức điều này là: bạn có thể bắt đầu xử lý dữ liệu của mình ngay cả trước khi dữ liệu được nhận khỏi mạng.

Đang tiếp tục tải xuống

Khi việc tải xuống bị tạm dừng hoặc bị gián đoạn, các đoạn dữ liệu đã đến sẽ được lưu trữ an toàn trong cơ sở dữ liệu IndexedDB. Khi đó, 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 bạn. Do máy chủ PWA Kino minh hoạ hỗ trợ các yêu cầu phạm vi HTTP tiếp tục tải xuống có phần đơ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 giấy, quá trình ghi giá trị dataChunk vào cơ sở dữ liệu IndexedDB rất đơn giản. Những giá trị đó đã là thực thể ArrayBuffer và có thể lưu trữ trực tiếp trong IndexedDB, vì vậy chúng ta chỉ có thể tạo một đối tượng có hình dạng thích hợp và lưu trữ nó.

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ể sẽ nhận thấy rằng IndexedDB của mình ghi chậm hơn đáng kể so với tốc độ tải xuống. Điều này không phải do IndexedDB ghi chậm, đó là do chúng tôi 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 đoạn dữ liệu mà chúng tôi nhận được từ mạng.

Các đoạn được tải xuống có thể khá nhỏ và có thể được luồng phát trong kế thừa nhanh chóng. Bạn cần giới hạn tốc độ ghi IndexedDB. Trong PWA Kino minh hoạ mà chúng tôi 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 đầu vào không phù hợp, chúng tôi đẩy toàn bộ bộ đệm vào cơ sở dữ liệu và hãy xoá dữ liệu đó trước khi thêm phần dữ liệu còn lại. Do đó, IndexedDB của chúng tôi việc ghi dữ liệu diễn ra ít thường xuyên hơn, dẫn đến cải thiện đáng kể khả năng ghi hiệu suất.

Phân phối tệp đa phương tiện từ bộ nhớ ngoại tuyến

Sau khi tải tệp nội dung nghe nhìn xuống, có thể bạn muốn trình chạy dịch vụ của mình 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ượng Response làm tham số.

  • Hàm khởi tạoResponse() 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 để tạo đối tượng Response: Blob, BufferSource, ReadableStream và các lý do khác.

  • Chúng ta cần một đối tượng không chứa tất cả dữ liệu của đối tượng đó trong bộ nhớ nên chúng ta sẽ có thể muốn chọn ReadableStream.

Ngoài ra, do chúng tôi đang xử lý các tệp lớn và chúng tôi muốn cho phép các trình duyệt chỉ yêu cầu một phần tệp mà họ hiện đang cần, chúng tôi cần triển khai một số hỗ trợ cơ bản cho các 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;

Vui lòng xem mã nguồn nhân viên dịch vụ của PWA Kino minh hoạ để tìm cách chúng ta đọc dữ liệu tệp từ IndexedDB và xây dựng luồng trong một ứng dụng thực tế.

Lưu ý khác

Bây giờ, bạn có thể bắt đầu thêm một số chướng ngại vật chính các tính năng hữu ích có cho ứng dụng video của bạn. Sau đây là một vài ví dụ về các tính năng mà bạn sẽ thấy trong PWA Kino minh hoạ:

  • Tích hợp API phiên nội dung nghe nhìn cho phép người dùng kiểm soát nội dung nghe nhìn phát bằng các phím phương tiện phần cứng chuyên dụng hoặc qua thông báo về nội dung nghe nhìn cửa sổ bật lên.
  • Lưu các tài sản khác được liên kết với các tệp đa phương tiện như phụ đề vào bộ nhớ đệm hình ảnh áp phích bằng API Bộ nhớ đệm cũ.
  • Hỗ trợ tải xuống luồng video (DASH, HLS) trong ứng dụng. Vì 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 có tải trước âm thanh và video.