You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
  });
}

修复点说明

  1. 添加event.respondWith()包裹Range请求逻辑:让Service Worker正式接管请求响应,离线时浏览器会等待我们的缓存处理结果,而不是直接发起失败的网络请求。
  2. 传递已缓存的响应到returnRangeRequest:避免重复查找缓存,提升性能同时减少潜在的缓存读取问题。
  3. 继承原缓存的Headers:不再硬编码Content-Type,适配不同格式的视频文件。
  4. 边界值修正:确保Range请求的起始/结束位置不会超出视频总字节数,避免slice操作出错。

修复后,离线状态下浏览器发起的第一个bytes=0-1元数据请求会得到正确的206响应,浏览器能解析出视频总时长,后续的Range请求也能正常被处理,视频就能正常播放了。

内容的提问来源于stack exchange,提问作者Shri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 08:49:09