HTTP视频流在Apple设备无法运行,Chrome正常的问题排查求助
Chrome正常但iOS无法加载视频的后端代码修复
问题场景
以下是接收客户端GET请求并发送视频的Node.js代码,在Chrome中运行正常,但iOS设备上无法加载视频且无报错:
const {name} = req.params; // console.log(req.headers) let range = req.headers.range console.log(req.headers); // if(!range) range = 'bytes=0-' if(req.headers.referer !== "https://course-client-nine.vercel.app/"){ return res.json({message: "No acces from another domain"}) } const myBucket = storage.bucket('coursebuckets'); const file = myBucket.file(name); if(!file){ return res.json({ message:"File not found", code:401 }) } const [metadata] = await file.getMetadata(); const videoSize = metadata.size; // const videoSize = fs.statSync('123.mp4').size const chunkSize = 1 * 1e+6; const start = Number(range.replace(/\D/g, '')); let end = Math.min(start+ chunkSize, videoSize - 1); let contentLength = end - start + 1; const headers = { "Content-Range": `bytes ${start}-${end}/${videoSize}`, "Accept-Ranges": 'bytes', "Content-Length" : contentLength, "Content-Type" : 'video/mp4' } res.writeHead(206,headers) const readStream = file.createReadStream({start, end}); readStream.pipe(res); readStream.on('error', (error) => { console.log(error); })
核心问题与修复方案
iOS Safari对视频流的处理比Chrome更严格,以下是针对性修复:
1. 兼容非Range请求
iOS Safari可能先发完整资源请求(不带Range头),当前代码未处理这种情况会直接报错。添加兼容逻辑:
let range = req.headers.range; // 处理不带Range头的请求,返回完整视频 if (!range) { const readStream = file.createReadStream(); res.writeHead(200, { "Content-Length": videoSize.toString(), "Content-Type": "video/mp4" }); readStream.pipe(res); return; }
2. 健壮解析Range头
当前用replace(/\D/g, '')的解析方式不兼容标准Range格式(比如bytes=0-1024),iOS会因解析错误拒绝加载。改用正则匹配标准格式:
// 匹配标准Range头格式:bytes=start-end const rangeMatch = range.match(/bytes=(\d+)-(\d+)?/); if (!rangeMatch) { return res.status(416).send("Requested Range Not Satisfiable"); } const start = Number(rangeMatch[1]); // 若Range未指定end,用chunkSize计算或取视频末尾 const end = rangeMatch[2] ? Number(rangeMatch[2]) : Math.min(start + chunkSize, videoSize - 1);
3. 修正文件存在性检查
storage.bucket.file()无论文件是否存在都会返回File对象,当前if(!file)永远不会触发,导致404判断失效。改用exists()方法检查:
const file = myBucket.file(name); const [exists] = await file.exists(); if (!exists) { return res.status(404).json({ message: "File not found", code: 404 }); }
4. 严谨处理跨域Referer检查
iOS某些场景下Referer可能为空(比如直接访问链接),当前代码会直接拦截导致请求失败。调整检查逻辑:
const allowedReferer = "https://course-client-nine.vercel.app/"; const referer = req.headers.referer; // 仅当Referer存在且不匹配允许域名时拦截 if (referer && !referer.startsWith(allowedReferer)) { return res.status(403).json({ message: "No access from another domain" }); }
5. 完善异步操作错误捕获
当前代码未捕获getMetadata()等异步操作的错误,一旦出错会导致请求挂起,iOS会直接终止加载。添加try/catch:
try { const [metadata] = await file.getMetadata(); const videoSize = Number(metadata.size); // ... 后续Range处理、响应逻辑 } catch (error) { console.error("Video stream error:", error); return res.status(500).json({ message: "Internal server error" }); }
6. 响应头细节修正
确保Content-Length是字符串类型,部分iOS版本对数字类型的响应头兼容性差:
const headers = { "Content-Range": `bytes ${start}-${end}/${videoSize}`, "Accept-Ranges": "bytes", "Content-Length": contentLength.toString(), "Content-Type": "video/mp4" };
完整修复后代码示例
const { name } = req.params; const allowedReferer = "https://course-client-nine.vercel.app/"; const chunkSize = 1 * 1024 * 1024; // 1MB,用更清晰的写法 try { // 跨域Referer检查 const referer = req.headers.referer; if (referer && !referer.startsWith(allowedReferer)) { return res.status(403).json({ message: "No access from another domain" }); } // 初始化GCS文件对象 const myBucket = storage.bucket("coursebuckets"); const file = myBucket.file(name); const [exists] = await file.exists(); if (!exists) { return res.status(404).json({ message: "File not found", code: 404 }); } // 获取文件元数据 const [metadata] = await file.getMetadata(); const videoSize = Number(metadata.size); // 处理Range请求 let range = req.headers.range; if (!range) { // 返回完整视频 const readStream = file.createReadStream(); res.writeHead(200, { "Content-Length": videoSize.toString(), "Content-Type": "video/mp4" }); readStream.pipe(res); return; } // 解析Range头 const rangeMatch = range.match(/bytes=(\d+)-(\d+)?/); if (!rangeMatch) { return res.status(416).send("Requested Range Not Satisfiable"); } const start = Number(rangeMatch[1]); const end = rangeMatch[2] ? Number(rangeMatch[2]) : Math.min(start + chunkSize, videoSize - 1); const contentLength = end - start + 1; // 设置分片响应头 const headers = { "Content-Range": `bytes ${start}-${end}/${videoSize}`, "Accept-Ranges": "bytes", "Content-Length": contentLength.toString(), "Content-Type": "video/mp4" }; // 发送分片响应 res.writeHead(206, headers); const readStream = file.createReadStream({ start, end }); readStream.pipe(res); readStream.on("error", (error) => { console.error("Stream error:", error); res.status(500).end(); }); } catch (error) { console.error("Video request error:", error); return res.status(500).json({ message: "Internal server error" }); }
内容的提问来源于stack exchange,提问作者Нурулла Аманнула
相关产品推荐
相关产品推荐

