You need to enable JavaScript to run this app. iOS Safari下Byte Range Request失效,Express+ytdl-core音频流无法播放
优惠活动
大模型
产品
解决方案
定价
更多

iOS Safari下Byte Range Request失效,Express+ytdl-core音频流无法播放

iOS Safari 无法播放YouTube音频流问题修复

Safari 主动发送Range: bytes=0-1的请求不是BUG,是WebKit内核加载媒体资源的标准校验逻辑,用于确认服务端支持字节范围请求、获取资源总大小,校验通过后才会发起后续的资源请求,这部分符合HTTP规范,不是问题根源。

现有代码的核心问题

  • 无Range请求时end值越界:媒体资源字节索引从0开始计数,总长度为contentLength时合法最大索引是contentLength - 1,原有代码无Range请求时直接将end赋值为contentLength,传给ytdl后会请求超出资源范围的内容,导致返回流长度和声明的Content-Length不匹配,Safari对该类校验要求极严格,直接触发加载失败。
  • 流传输未处理背压:手动监听readable事件读取并写入响应,没有处理传输背压,容易出现卡顿、断流,Safari遇到传输异常会直接终止加载,不会自动重试。
  • 异步错误未捕获:async路由中直接抛出错误未被Express捕获,会返回无响应体的500错误,触发Safari加载失败。
  • MIME类型兼容问题:部分ytdl返回的audioonly格式container为m4a,audio/m4a不是Safari认可的标准MIME类型,需要替换为audio/mp4才能正常识别。

修复后代码

app.get("/stream/:videoId", async (req, res, next) => {
  try {
    const {videoId} = req.params;
    const isValid = ytdl.validateID(videoId);
    if (!isValid) return res.sendStatus(400);

    const videoInfo = await ytdl.getInfo(videoId);
    // 优先选m4a容器的音频,避开Safari不支持的webm格式
    let audioFormat = ytdl.chooseFormat(videoInfo.formats, {
      filter: format => format.audioCodec && !format.videoCodec && format.container === 'm4a',
      quality: "highestaudio"
    });

    const {itag, container, contentLength} = audioFormat;
    let start = 0;
    let end = contentLength - 1;
    let contentRange = false;
    const range = req.headers.range;

    if (range) {
      const positions = range.replace(/bytes=/, "").split("-");
      start = parseInt(positions[0], 10);
      end = positions[1] ? parseInt(positions[1], 10) : contentLength - 1;
      // 边界校验,避免请求超出资源范围
      if (start >= contentLength || end >= contentLength) {
        res.setHeader('Content-Range', `bytes */${contentLength}`);
        return res.sendStatus(416);
      }
      contentRange = true;
    }

    const chunksize = end - start + 1;
    const responseCode = contentRange ? 206 : 200;
    // 兼容Safari的MIME类型
    const contentType = container === 'm4a' ? 'audio/mp4' : `audio/${container}`;
    const responseHeader = {
      "Accept-Ranges": "bytes",
      "Content-Length": chunksize,
      "Content-Type": contentType,
      // 额外添加安全头,避免Safari跨域拦截
      "Access-Control-Allow-Origin": "*"
    };
    if (contentRange) {
      responseHeader["Content-Range"] = `bytes ${start}-${end}/${contentLength}`;
    }
    res.writeHead(responseCode, responseHeader);

    // 直接用pipe处理流传输,自动处理背压
    const stream = ytdl(videoId, {
      filter: format => format.itag === itag,
      range: {start, end}
    });
    stream.pipe(res);
    stream.on('error', err => {
      console.error('流传输错误:', err);
      res.end();
    });
  } catch (err) {
    // 捕获异步错误,交给Express错误处理
    next(err);
  }
});

额外注意事项

如果需要支持多容器格式,需要在chooseFormat阶段就过滤掉iOS Safari不支持的webm、opus等编码格式,避免返回无法解码的资源。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.18 16:15:04