渐进式 Web 应用引入了之前专为原生应用预留的许多功能, 将应用部署到 Web 应用。Google Cloud 中 PWA 是一种离线体验。
更好的是离线流媒体体验, 您可以通过几种不同的方式为用户提供的增强功能。不过, 这会造成一个非常独特的问题:媒体文件可能非常大。因此 您可能会问:
- 如何下载和存储大型视频文件?
- 如何向用户提供?
在本文中,我们将讨论这些问题的答案, 参考我们构建的 Kino 演示版 PWA,该 PWA 可为您提供实用的 如何在不启用在线媒体服务的情况下 使用任何功能性框架或展示框架进行构建。以下示例为 主要用于教育目的,因为在大多数情况下,您应该使用 某个现有 MediaFrame 提供这些功能。
除非您有充分的业务案例来开发自己的 PWA,否则构建 PWA 都面临着挑战。在本文中,您将了解 用于为用户提供高品质离线媒体的 API 和技术 体验
下载和存储大型媒体文件
渐进式 Web 应用通常使用便捷的 Cache API 进行下载 并存储提供离线体验所需的资源:文档、 样式表、图片等。
以下是在 Service Worker 中使用 Cache API 的一个基本示例:
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',
]);
})
);
});
虽然以上示例在技术上是可行的,但使用 Cache API 有几个 存在一些限制,因此使用大型文件并不现实。
例如,Cache API 不会:
- 可让您轻松暂停和继续下载
- 跟踪下载进度
- 提供一种正确响应 HTTP 范围请求的方法
所有这些问题对任何视频应用来说都是相当严重的限制。 我们来了解一下其他可能更合适的选项。
如今,Fetch API 是一种跨浏览器异步访问远程 文件。在我们的用例中,它允许您以串流形式访问大型视频文件, 使用 HTTP 范围请求将它们以数据块的形式逐步存储。
现在,您可以使用 Fetch API 读取数据块;此外,您还需要 存储它们。您的媒体可能有很多相关的元数据 例如:名称、说明、运行时长度、类别等。
您存储的不是一个媒体文件,而是存储了一个结构化对象 媒体文件只是其属性之一
在这种情况下,IndexedDB API 就是一个绝佳的解决方案, 媒体数据和元数据它可以轻松存储大量二进制数据, 还提供索引,可让您执行非常快速的数据查询。
使用 Fetch API 下载媒体文件
我们在演示 PWA 中构建了一些关于 Fetch API 的有趣功能, (我们将其命名为 Kino),其源代码是公开的,因此您随时可以查看。
- 可以暂停和恢复未完成的下载。
- 用于在数据库中存储数据块的自定义缓冲区。
在展示这些功能的实现方式之前,我们先介绍 快速回顾如何使用 Fetch API 下载文件。
/**
* 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);
}
注意到 await reader.read()
处于循环状态吗?这是您接收数据块的方式
从网络到达的可读取流中数据的 BERT 模型。考虑如何
很实用,您可以在数据全部送达之前就开始对其进行处理
。
继续下载
当下载暂停或中断时,已经到达的数据块将会 安全地存储在 IndexedDB 数据库中。然后,您可以显示一个按钮 在您的应用中继续下载。由于 Kino 演示 PWA 服务器 支持 HTTP 范围请求恢复下载的操作比较简单:
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);
}
IndexedDB 的自定义写入缓冲区
在纸面上将 dataChunk
值写入 IndexedDB 数据库的过程
非常简单。这些值已经是 ArrayBuffer
实例,可以存储
直接存储在 IndexedDB 中,因此我们只需创建一个具有适当形状的对象
并加以存储
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 = () => { ... }
虽然这种方法有效,但您可能会发现,IndexedDB 会写入 明显比下载慢得多。这并不是因为 IndexedDB 会写入 那是因为我们通过为 Cloud Shell 创建 为我们从网络收到的每个数据块执行新事务。
下载的数据块可以非常小,并可以通过流在 快速连续发生。您需要限制 IndexedDB 写入的速率。在 Kino 演示版 PWA 通过实现中间写入缓冲区来实现此目的。
当数据块从网络到达时,我们首先将其附加到缓冲区。如果 传入的数据不合适,我们就会将完整的缓冲区刷新到数据库中, 请先清除这些数据,然后再附加其余数据。因此,IndexedDB 这样可以显著改善写入 性能
从离线存储空间传送媒体文件
下载媒体文件后,您可能希望 Service Worker 通过 IndexedDB 提供文件,而不是从网络中提取文件。
/**
* 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);
那么,您需要在getVideoResponse()
中做些什么?
event.respondWith()
方法需要Response
对象作为参数。Response() 构造函数显示有几种类型的对象, 可用于实例化
Response
对象:Blob
、BufferSource
、ReadableStream
等。我们需要一个对象不把所有数据都保存在内存中,所以 建议选择
ReadableStream
。
另外,由于我们要处理大文件,并希望允许浏览器 只请求他们当前需要的那部分文件,我们需要实现 为 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;
请随时查看 Kino 演示 PWA Service Worker 源代码,以查找 如何使用 IndexedDB 构建流, 真实应用
其他注意事项
清除主要障碍后,您现在可以开始添加 为视频应用增加一些值得期待的功能。下面是一些 Kino 演示版 PWA 中提供的以下功能:
- Media Session API 集成,可让用户控制媒体 使用专用硬件媒体键或媒体通知进行播放 弹出式窗口。
- 缓存与媒体文件相关的其他资源(如字幕),以及 海报图片。
- 支持在应用内下载视频流(DASH、HLS)。因为在线播放 通常会声明不同比特率的多个来源,因此您需要 转换清单文件,并且只下载一个媒体版本,然后再存储 以供离线观看
接下来,您将了解如何通过预加载音频和视频实现快速播放。