Strapi后端MP4视频在Safari无法播放(Chrome正常,疑似CORS相关问题)
我之前也踩过Safari播放Strapi托管视频的坑,这个AbortError十有八九和Safari严格的Range请求依赖、视频编码兼容性或者跨域细节有关,给你几个针对性的排查和解决方向:
一、先确认Strapi是否正确处理Range请求
Safari不像Chrome那样能容忍完整文件加载,它强制要求视频服务支持Range分段请求,如果Strapi返回的是200 OK而不是206 Partial Content,或者响应头缺少必要字段,直接就会 abort 加载。
用curl测试Range请求:
curl -I -H "Range: bytes=0-100" "你的完整视频URL"正常的响应应该包含:
- 状态码
206 Partial Content - 响应头
Accept-Ranges: bytes和Content-Range: bytes 0-100/[文件总大小]
- 状态码
如果Strapi默认的静态文件服务没正确处理Range,你可以加个自定义中间件补全:
在src/middlewares/range-handler.js里写:module.exports = (strapi) => { return { initialize() { strapi.app.use(async (ctx, next) => { await next(); if (ctx.method === 'GET' && ctx.url.includes('/uploads/') && ctx.response.status === 200) { const range = ctx.headers.range; if (range) { const fs = require('fs'); const path = require('path'); const filePath = path.join(strapi.dirs.static.public, ctx.path); try { const fileSize = fs.statSync(filePath).size; const parts = range.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunksize = (end - start) + 1; const file = fs.createReadStream(filePath, {start, end}); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': ctx.response.type, }; ctx.response.status = 206; ctx.response.set(head); ctx.body = file; } catch (err) { console.error('处理Range请求失败:', err); } } } }); }, }; };然后在
config/middlewares.js里把这个中间件加到strapi::cors后面。
二、检查视频文件的编码是否符合Safari标准
Safari对MP4的编码要求非常严格,必须满足:
- 视频编码:H.264(Baseline/Main/High Profile都可)
- 音频编码:AAC-LC
- moov原子(视频元数据)必须在文件开头(“快启动”格式)
用ffprobe检查编码:
ffprobe -v error -show_entries stream=codec_name,codec_type "你的本地视频文件"输出应该类似:
[STREAM] codec_type=video codec_name=h264 [/STREAM] [STREAM] codec_type=audio codec_name=aac [/STREAM]如果moov原子在文件末尾,用ffmpeg转码修正:
ffmpeg -i input.mp4 -movflags faststart output.mp4转完后重新上传到Strapi,这一步我当时解决了80%的Safari播放问题!
三、调整跨域和缓存的细节
把Strapi的CORS origin从
*改成具体的前端域名,Safari对通配符*的跨域处理比Chrome严格:// config/middlewares.js { name: 'strapi::cors', config: { origin: ['http://localhost:3000'], // 不要用* methods: ['GET', 'HEAD', 'OPTIONS'], headers: ['Content-Type', 'Authorization', 'Range', 'If-Range'], // 加上If-Range exposeHeaders: ['Content-Length', 'Content-Range', 'Accept-Ranges'], credentials: false, keepHeaderOnError: true, }, }临时绕过Safari的缓存测试:
在你的代码里给URL加个随机时间戳,彻底绕过缓存(仅测试用):const src = data.url ? `${getAssetUrlPrefix()}${data.url}?t=${Date.now()}` : '';如果这样能播放,说明是缓存导致的,那就在Strapi的响应头里加合适的Cache-Control:
在自定义中间件里补充:ctx.set('Cache-Control', 'public, max-age=31536000, immutable');
四、代码层面的小调整
把
preload="auto"改成preload="metadata",Safari的preload="auto"会提前发起Range请求,有时候会和跨域缓存冲突:<video controls crossOrigin="anonymous" muted playsInline preload="metadata" <!-- 修改这里 --> ref={videoRef} width="100%" poster={`${posterData?.url ? getAssetUrlPrefix() + posterData.url : ''}`} >给video标签加详细的错误捕获,看看具体的错误码:
const handleVideoError = (e) => { const error = e.target.error; console.error('Safari视频错误详情:', { code: error.code, message: error.message }); // 错误码对应:MEDIA_ERR_NETWORK(网络/跨域)、MEDIA_ERR_DECODE(编码问题)等 }; // 在useEffect里绑定 useEffect(() => { const videoElement = videoRef.current; if (!videoElement) return; // ... 其他监听 videoElement.addEventListener('error', handleVideoError); return () => { // ... 其他移除监听 videoElement.removeEventListener('error', handleVideoError); }; }, []);
五、如果用了第三方存储(比如S3/Cloudinary)
如果你的视频不是存在Strapi本地,而是第三方存储,那问题可能在存储服务的配置:
- 确保存储服务的CORS规则允许
Range、Content-Range、If-Range头 - 确保存储服务支持Range请求返回206状态码(比如S3默认支持,但要确认Bucket Policy允许GetObject)
建议你先从视频编码检查和Range请求测试开始,这两个是Safari播放MP4最常见的坑,我当时就是因为moov原子在文件末尾,转码后直接就好了!




