踩坑记录:HTTPS 页面下 xgplayer 拉流被 Mixed Content 屏蔽
更新: 5/1/2026 字数: 0 字 时长: 0 分钟
一、问题背景
最近在做一个基于 xgplayer 的直播播放页面。项目有几个比较特殊的约束:
- 双版本共存:
package.json里同时装了xgplayer@2.17.10和通过 npm alias 装的xgplayer-3(实际是xgplayer@3.0.21-rc.10),历史业务绑在 v2,新功能想用 v3。 - 直播插件
xgplayer-flv-live@3.0.0-alphatt.141只兼容 v3,默认import 'xgplayer'会解析到 v2,所以先处理了一波补丁(用 patch-package 把它里面的'xgplayer'改成'xgplayer-3')。 - 页面运行在 HTTPS 环境(内部系统走的是公司统一的 HTTPS 域名)。
等插件这边跑通,进到真正播放环节,新的坑就来了。
二、问题现象与初始猜测
打开页面,视频区一片黑,DevTools Network 里看到直播流请求的状态赫然写着:
(已屏蔽: mixed-content)请求根本没有发出去,不是超时、不是 CORS、不是 4xx/5xx,而是浏览器层直接拦截。
一开始我的判断有点跑偏:
- 第一反应是"是不是 xgplayer-flv-live 的补丁没打好,import 路径错了导致拉流失败?"
- 然后怀疑"是不是 CDN 那边证书/鉴权出问题?"
- 又怀疑"是不是 v2/v3 冲突,把 flv 相关的 worker 加载坏了?"
但仔细看那个 "已屏蔽: mixed-content" 的字样,才意识到这是浏览器安全策略层面的问题,跟我 xgplayer 本身没啥关系——是协议不匹配。
三、踩坑过程
尝试 1:怀疑是插件补丁问题
先回头检查 patch-package 生成的 patch,确认 xgplayer-flv-live 里所有 'xgplayer' 都改成了 'xgplayer-3',构建产物里也确认 v3 被正确引入。结论:和插件无关,排除。
尝试 2:认真看报错,定位到 Mixed Content
回到那一行 "已屏蔽: mixed-content",翻出浏览器的混合内容规则才想起来:
| 页面协议 | 资源协议 | 行为 |
|---|---|---|
| HTTPS | HTTPS / WSS | 放行 |
| HTTPS | HTTP(fetch/XHR/WS,主动混合) | 直接屏蔽 |
| HTTPS | HTTP(img/video src,被动混合) | 警告但可能加载 |
| HTTP | 任意 | 放行 |
HTTP-FLV 本质是 fetch 流式拉取,属于主动混合内容,浏览器不给"仍要加载"的选项,直接拍死。我的页面是 HTTPS,拉流地址是 http://xxx/live.flv,所以必挂。
尝试 3:想过但没走的几条路
- 把页面降级为 HTTP:只对本地调试友好,线上不可能,放弃。
- 加
upgrade-insecure-requestsCSP:能把http://自动升级成https://,但前提是源站 HTTPS 真的可用;而且对ws://不生效。当时没 100% 确认 CDN 两侧都开好,先搁置。 - 自建 HTTPS 反代:带宽成本高,只适合应急,纯直播场景属于杀鸡用牛刀,pass。
- 推动直播服务方开通 HTTPS,然后前端写死
https://:最干净的根治方案,但涉及跨团队沟通 + 改代码,短期内没法立刻上。
尝试 4:最终命中的「零成本解法」
翻资料时想起协议相对 URL(Protocol-relative URL)这个老写法:把流地址里的 http: 去掉,只保留 //:
//cdn.xxx.com/live/stream.flv浏览器会按当前页面协议自动补齐:HTTP 页面下发 HTTP 请求,HTTPS 页面下发 HTTPS 请求。试了一下,HTTPS 页面下正常播放,HTTP 入口也没挂——一行改动搞定。
能成功的隐含前提是:CDN 源站本来就同时开了 HTTP 和 HTTPS,只是我们代码里硬编码了 http:// 才触发 mixed-content。改成 // 后直接命中 HTTPS,问题消失。
四、多种解决方案对比
| 方案 | 实现复杂度 | 侵入性 | 性能/资源 | 可维护性 | 风险 |
|---|---|---|---|---|---|
A. 源站升级 HTTPS,前端写死 https:// | 中(要推动服务方) | 低(只改 URL) | 最优 | 最好,显式清晰 | 无,根治 |
B. 协议相对 URL //xxx | 极低(删两字符) | 极低 | 最优 | 一般(隐式依赖双栈) | file:// 下失效;WS 不适用;HTTP 入口仍走明文 |
C. CSP upgrade-insecure-requests | 低(加一行 meta/header) | 低 | 最优 | 中 | 源站必须真支持 HTTPS;对 ws:// 无效 |
| D. 自建 HTTPS 反代 | 高(要搭 Nginx/部署) | 高(基建) | 差(全量过代理,占带宽) | 差 | 单点故障、成本高 |
| E. 页面降级 HTTP | 极低 | - | - | - | 线上不可接受,仅调试 |
| F. Chrome 启动参数绕过 | 极低 | - | - | - | 只对自己生效,不能给用户 |
关键几条:
- A 是长期最优解,但依赖外部配合。
- B 是当下最快解,一行改动,HTTPS/HTTP 双栈通吃,适合过渡期。
- C 在源站 HTTPS 已稳定时是更显式的 B,但对 WebSocket 场景失效。
- D/E/F 都是非生产级兜底,不做首选。
五、最终采用的方案
选了:协议相对 URL(方案 B)
把直播地址从:
- http://cdn.xxx.com/live/stream.flv
+ //cdn.xxx.com/live/stream.flv一次性替换掉代码里所有硬编码的 http:// 拉流地址。
关键实现要点
- 统一从配置/接口下发流地址,前端收到后先做一次规范化:js
function normalizeStreamUrl(url) { return url.replace(/^https?:/, ''); // 保留 //host/path } - WebSocket 场景单独处理(协议相对写法对
ws://无效):jsconst wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProto}//xxx.com/live`; - 本地开发注意:如果直接
file://双击打开 HTML,//cdn.xxx.com会被当成file://cdn.xxx.com,必挂;务必用 dev-server 跑。 - 回归验证:分别在 HTTPS 线上域名 + HTTP 内网入口各过一遍,确认都能拉流。
为什么选它而不是 A
- A 需要联系直播服务方开证书、配置、回归,周期至少几天,而我的需求是当天得让页面能播。
- B 是零成本、零风险(源站双栈已存在)、改动量最小的方案,完美适配"救火"节奏。
- 长期还是会推动 A,但 B 能作为过渡期的稳定兜底,两条路不冲突。
六、经验总结与踩坑教训
一开始哪里判断失误
- 看到直播播不出来,下意识往业务代码/插件/CDN 上查,忽略了浏览器那行字面意思非常清楚的 "mixed-content"。
- 对"主动混合内容"和"被动混合内容"的区别没概念,以为
<video src="http://...">能加载,就觉得 fetch 流也能加载——其实规则完全不同。
可迁移的经验
- "已屏蔽"类网络错误先看浏览器安全策略。Network 面板里凡是状态不是 HTTP 状态码而是中文括号的(已屏蔽/blocked/CORS error/...),80% 是浏览器层的拦截,不是业务错,先查策略再查代码。
- Mixed Content 的两档策略要记牢:主动混合(fetch/XHR/WS/script)无条件拦截,被动混合(img/video/audio)只警告。直播流、WebSocket、接口全属前者。
- 协议相对 URL
//host/path是过渡期神技,但有三个坑:file://不能用、WebSocket 不支持、HTTP 入口下仍走明文。 - 能根治就别只打补丁:源站 HTTPS 化 + 显式
https://才是长期方案,Google 早年就不推荐 protocol-relative 了。B 方案只是抢时间的过渡。 - 多版本包共存 + 插件补丁这种组合要格外小心,出问题时先用排除法(确认补丁、版本、打包产物都对),再去看更上层的浏览器/网络问题,否则很容易被表象带偏。