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

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,提问作者Нурулла Аманнула

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 11:35:21