iOS Safari离线模式下视频无法播放问题排查:Service Worker缓存视频的Range请求处理异常
问题分析与修复方案
你的核心问题出在Range请求分支没有正确调用event.respondWith(),导致Service Worker无法拦截并响应离线时的Range请求,浏览器得不到视频元数据就会卡在0时长状态。下面具体拆解问题和修复步骤:
核心问题拆解
看你原代码的Range请求处理分支:
if (event.request.headers.get("range")) { caches.match(event.request.url).then(function (res) { if (!res) { log.debug(...) return fetch(event.request); } returnRangeRequest(event); }); }
这里的致命问题是:你没有用event.respondWith()包裹整个异步逻辑。Service Worker的fetch事件必须通过event.respondWith()来告诉浏览器“我要接管这个请求的响应”,否则浏览器会直接走默认的网络请求流程——离线时网络请求失败,第一个bytes=0-1的元数据请求就得不到有效响应,浏览器自然无法解析视频时长。
除此之外,代码还有几个次要问题:
- 重复调用
caches.match()查找缓存,浪费性能 - 硬编码
Content-Type,适配性差 - 没有处理Range参数的边界情况(比如请求的字节范围超出视频总长度)
修复后的完整代码
self.addEventListener("fetch", function (event) { if (event.request.headers.get("range")) { // 关键:用event.respondWith包裹整个Range请求处理流程 event.respondWith( caches.match(event.request.url).then(function (cachedResponse) { if (!cachedResponse) { log.debug( `Range request NOT found in cache for ${event.request.url}, activating fetch...` ); return fetch(event.request); } // 直接传入已找到的缓存响应,避免重复查找 return returnRangeRequest(event, cachedResponse); }).catch(err => { log.error(err); return fetch(event.request); // 兜底逻辑,离线时会失败但至少有错误提示 }) ); } else { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request); }) ); } }); function returnRangeRequest(event, cachedResponse) { const rangeHeader = event.request.headers.get("range"); const rangeMatch = rangeHeader.match(/^bytes\=(\d+)\-(\d+)?/); // 处理非法Range格式的情况 if (!rangeMatch) { return cachedResponse; } let pos = Number(rangeMatch[1]); const pos2 = rangeMatch[2] ? Number(rangeMatch[2]) : undefined; return cachedResponse.arrayBuffer().then(function (ab) { const totalBytes = ab.byteLength; // 修正Range参数的边界,避免超出视频总长度 pos = Math.min(pos, totalBytes - 1); const end = pos2 ? Math.min(pos2, totalBytes - 1) : totalBytes - 1; const contentLength = end - pos + 1; // 继承原缓存响应的headers,再修改Range相关字段(替代硬编码) const responseHeaders = new Headers(cachedResponse.headers); responseHeaders.set("Content-Range", `bytes ${pos}-${end}/${totalBytes}`); responseHeaders.set("Content-Length", contentLength.toString()); const abSliced = ab.slice(pos, end + 1); log.debug(`Returning range request response: bytes ${pos}-${end}/${totalBytes}`); return new Response(abSliced, { status: 206, statusText: "Partial Content", headers: responseHeaders }); }).catch(function (err) { log.error(err); // 出错时返回完整缓存响应作为兜底 return cachedResponse; }); }
修复点说明
- 添加
event.respondWith()包裹Range请求逻辑:让Service Worker正式接管请求响应,离线时浏览器会等待我们的缓存处理结果,而不是直接发起失败的网络请求。 - 传递已缓存的响应到
returnRangeRequest:避免重复查找缓存,提升性能同时减少潜在的缓存读取问题。 - 继承原缓存的Headers:不再硬编码
Content-Type,适配不同格式的视频文件。 - 边界值修正:确保Range请求的起始/结束位置不会超出视频总字节数,避免
slice操作出错。
修复后,离线状态下浏览器发起的第一个bytes=0-1元数据请求会得到正确的206响应,浏览器能解析出视频总时长,后续的Range请求也能正常被处理,视频就能正常播放了。
内容的提问来源于stack exchange,提问作者Shri
相关产品推荐
相关产品推荐

