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
相关产品推荐
相关产品推荐

