需求背景及简述
视频配音,需要根据开始和结束时间分段播放视频,涉及视频跳至某一时刻的操作。另因需要在播放视频的时候同时进行录音,所以希望视频在播放的时候最好不要出现等待数据加载的情况,这样就要求在答题开始前完整下载整个视频。
解决过程
完整下载视频
以前有个误解,只要设置video的preload属性,它便会自动将整个视频下载下来。但事实并非如此,目前的方法是通过XHR请求视频资源。
这样出现了一个小问题,原来我们的视频资源是没有对页面所在的域名增加跨域允许的,此问题通过联系CDN服务商增加Access-Control-Allow-Origin响应头解决。
值得注意的是,我们经常看到Access-Control-Allow-Origin会设为*(允许所有域名),但如果是只对特定域名的话,除了域名以外还需要添上其协议,如www.abc.com需要改为https://www.abc.com。
缓存视频
视频不能缓存的原因
视频是可以下载下来了,但却发现每次重新进入页面时,资源并没有被缓存,都是需要重新请求下载,这无疑会影响载入速度。尝试寻找原因,先看看视频的响应头是否有缓存控制相关的(是有的),实际在浏览器中也能正确地被缓存。
与图片,JS等资源可以正确地被缓存相比,那视频为什么没被webview缓存呢?
这里可以提出两个假设:
webview默认只会缓存特定扩展名的资源webview默认对缓存资源的大小有限制
验证假设1,我们可以修改请求资源的url来改为其他扩展名,然后再代理到实际的文件。使用whistle可以配置如下的规则
1 | cdn-resource.abc.com/acpf/data/upload/bt/2021/07/22/60f934011029c.png cdn-resource.abc.com/acpf/data/upload/bt/2021/07/22/60f934011029c.mp4 |
经试验,假设1不成立。
验证假设2,我们先将资源下载到本地,然后使用ffmpeg截取视频的头一秒作为新视频,示例命令如下:
1 | ffmpeg -i 60f934011029c.mp4 -ss 00:00:00 -t 00:00:01 out.mp4 |
然后配置
whistle如下规则1 | cdn-resource.abc.com/acpf/data/upload/bt/2021/07/22/60f934011029c.mp4 file://D:\MyDownloads\out.mp4 resCors://* |
经试验,资源可被正确缓存,假设2成立
缓存方案
我们也许可以让本地开发调整相关配置来开放此限制,但这个问题并非必须寻求本地开发的协助,web端也有本地缓存的方案,所以不妨仅使用web端的能力来解决问题。
网上查找相关的方案,主要可以找到这几个技术ApplicationCache,service worker, indexedDB。
ApplicationCache在Android的文档和caniuse上都有说这是个废弃的API,并将会在新版本移除,推荐使用新标准的service worker。
查阅caniuse,发现service worker至少在safari 11.3上才获得支持。注意service worker本身并不是缓存API cache本身,chrome43版本以后页面环境也可以使用cache API了,但service worker原生支持对请求的劫持,所以不妨折腾多一点,把service worker也引入进来。前面说到service worker和cache有兼容问题,所以需要找到一个fallback的方案。
这里就到indexedDB登场了,indexedDB不像localstorage只能储存字符串,还可以存储对象(例如存储blob对象),indexedDB在safari 8就已支持。
这两种离线存储的空间限制,从网上的资料上看,大概各个浏览器的实现都不太一样,但基本只是跟设备的物理储存挂钩。
方案实现
以下是service woker和indexedDB的实现代码
service worker涉及页面service worker的注册和service worker的worker主逻辑的编写。
以下代码基本来源自create-react-app中的模板代码
注册service worker的逻辑,提供register和unregister两个方法。值得注意的是,navigator.serviceWorker.register的第二个参数,scope我们设为了/。scope是相对于当前页面地址的路径,默认为空,即当前地址,表示它可控制以这个地址为前缀的页面。
如果是/开头表示直接相对于域名。例如scope为/hello,域名为www.abc.com,那么则表示它能控制www.abc.com/hello为前缀的页面。所以这里设为/可以让我们控制这个域名下的所有页面。
但这里会有个问题,我原来没有想到的。servie worker只要注册了就会常驻在浏览器中的,所以即便在不是当时注册了它的页面下,只要在其scope的控制范围,它也会生效,这样就有可能不必要地在其他页面中出现。
另外有两个值得注意的点service worker需要放在与页面相同的域名下。scope的设置范围默认是不能高于service worker的url地址。
例如www.abc.com/resource/service-worker.js,它不能设为/, 至多只能为/resource。要解除这个限制,可以在返回service worker文件的响应头中添加service-worker-allowed: : /。现在是在nginx中添加的。
1 | // serviceWorker.ts |
完成这次缓存任务的worker的逻辑代码。代码中引入workbox系列的工具库。
clientsClaim是通知注册此worker的页面,自己将开始控制页面
registerRoute是注册拦截请求的处理函数,处理函数的返回类型是Promise<response>。
1 | /// <reference lib="webworker" /> |
- 下面是
indexedDB的实现,indexedDB因为API比较冗余,所以一般会用Dexie.js这个库,不过因为我之前已经在其他项目写过indexedDB的wrapper,所以这里我用自己的实现(地址)。
另外我还专门为这次的需求,封装了个DBCache类,也仿照workbox实现了maxEntries。地址。
使用indexedDB的时候,需要将XHR的responseType设为blob。Blob可以通过URL.createObjectURL转为临时地址供video使用
总结
在解决问题的过程中,再次熟悉了service worker和indexedDB的使用,是一次充实的学习经历。另:本文忽略了service worker的indexedDB的API的讲解,读者可以搜索相关的文档补足。