Appearance
怎么选视频源
本页摘自仓库的视频源配置指南,构建时 @include 抽取,零漂移同步。
如果你只想看结论:直播传
[FLV, HLS]两种,点播传HLS一种。 但强烈建议读完"为什么"——源传少了会在部分手机上必然播不了,而你在自己电脑上测不出来。
🚦 一句话速查
| 场景 | 覆盖手机(含 iOS) | 只覆盖桌面 |
|---|---|---|
| 直播 | FLV + HLS 两种 | FLV 一种即可 |
| 点播 | HLS 一种 | HLS 一种 |
js
// ✅ 直播 · 要覆盖手机
source: {
live: true,
sources: [
{ url: 'https://cdn/live.flv', type: 'flv' },
{ url: 'https://cdn/live.m3u8', type: 'hls' },
],
}
// ✅ 点播 · 全场景
source: { url: 'https://cdn/vod.m3u8', type: 'hls' }但请务必读完 §1 再决定"只覆盖桌面" —— 这个判断比你想的容易出错。
一、先决定:你的用户在哪
传几种源不是技术问题,是覆盖范围问题。 先回答这个,后面的规则才有意义。
1.1 决策表
| 你的用户范围 | 直播必传 | 理由 |
|---|---|---|
| 公网 C 端(有手机流量) | FLV + HLS | 必然有 iOS 用户 |
| 会在微信里被打开 | FLV + HLS | 微信 WebView 强制 HLS |
| 企业内网 PC 端 | FLV 即可 | 环境可控,没有 iOS |
| 监控大屏 / Kiosk | FLV 即可 | 设备固定 |
| 不确定 | FLV + HLS | 多一路流的成本 << 线上事故 |
1.2 "只支持桌面,是不是 FLV 一种就够?"
技术上:是的。 桌面 Chrome / Edge / Firefox / macOS Safari 全都有 MSE,flv.js 跑得起来, requiresHls 判定为 false,FLV 正常播放。
但"只支持桌面"这个前提比你想的脆弱。 下面任意一条成立,这个前提就破了:
- ❌ 页面链接被人转发到微信里点开 → 微信 WebView 强制 HLS → 报错
- ❌ 用户用 UC / 夸克浏览器访问(这两个有桌面版)→ 强制 HLS → 报错
- ❌ 运营做了个移动端 H5 入口,复用了同一个播放页
- ❌ 老板用 iPad 看数据大屏
判断标准:你能不能百分之百保证"URL 永远不会在 iOS 或微信/UC/夸克里被打开"?
- 能(内网系统、固定设备、有登录态且强制客户端)→ FLV 一种可以
- 不能(任何公网可访问的页面)→ 老老实实传两种
1.3 传两种源的成本
诚实地说,这不是免费的 —— 后端要同时推两路流:
| 成本项 | 说明 |
|---|---|
| 转码 | 一路源流 → 同时输出 FLV + HLS(多数流媒体服务器如 SRS / nginx-rtmp 原生支持,配置项而已) |
| 带宽 | 只有实际被拉取的那一路产生带宽,不是翻倍 |
| 存储 | 直播不落盘的话基本无额外存储 |
结论:主要成本是转码配置,一次性工作。相比"iOS 用户全部黑屏"的事故,这个成本可以忽略。
二、第一层分类:点播 还是 直播
整个选源策略的第一个分叉就是 live 字段。它不是元信息,它直接翻转优先级数组 (packages/player-core/src/plugins/source-router.ts:85-87):
js
const order = source.live
? ['flv', 'hls', 'mp4'] // 直播:FLV 延迟最低
: ['hls', 'mp4', 'flv'] // 点播:HLS 功能最全直播 live: true | 点播 live: false(默认) | |
|---|---|---|
| 首选 | FLV | HLS |
| 核心诉求 | 低延迟 | 多码率 + 可 seek |
| 必传 | FLV + HLS | HLS |
⚠️
live默认false。直播忘了传live: true,会按点播优先级选 HLS —— 能播,但白白多了十几秒延迟,而且不报错,很难发现。⚠️ 字符串写法永远是点播:
source: 'https://cdn/live.flv'的live恒为false。 直播请务必用对象写法。
三、直播:为什么必须传两种
要理解这条规则,得先理解两件事:为什么直播优先 FLV,以及为什么 iOS 只能 HLS。
3.1 为什么直播优先 FLV —— 延迟的原理差异
这是协议机制决定的,不是实现优劣:
HLS 的延迟来自"切片":
推流 → 服务器攒够一个切片(通常 2-6s)→ 写完 → 更新 m3u8 → 播放器拉取
↑ 必须等切片完整写完才能下发
播放器通常还要缓冲 3 个切片才起播 → 延迟 = 切片时长 × 3典型 HLS 延迟:6-30 秒。切片切得越小延迟越低,但请求数暴涨、CDN 压力大。
FLV 是流式长连接:
推流 → 服务器 → HTTP 长连接边推边转发 → 播放器边收边解
↑ 没有"攒切片"这一步典型 HTTP-FLV 延迟:1-3 秒。
所以:互动直播(带货、连麦、答题)必须 FLV;慢直播(风景、监控回看)HLS 也能接受。
💡 LL-HLS 是第三条路:通过
EXT-X-PART把切片再细分成"分片",延迟能压到 2-5s。 代价是需要 HTTP/2 chunked 传输的 CDN 支持。见 §6.3。
3.2 为什么 iOS 不能承诺 FLV —— flv.js 所需的路径不存在
FLV 在浏览器里播放的唯一路径:
FLV 流 → flv.js 在 JS 里解封装 → 转成 fMP4 片段 → 喂给 MediaSource → <video> 解码
↑ 关键依赖iOS 不暴露 flv.js 所需的标准 window.MediaSource 路径。iOS 上所有浏览器(包括 iOS 版 Chrome、Firefox)使用 WebKit;iOS 17.1+ 的 ManagedMediaSource 让 hls.js 可以播放 HLS, 但它不是 flv.js 可用的标准 MSE 接口,不能把 FLV 解封装后的 fMP4 直接作为该库的全平台播放路径。
没有 MSE,flv.js 连初始化都过不去 —— 它会直接检测 window.MediaSource 并拒绝启动。
而且没有原生兜底:Safari 的 <video> 根本不认 FLV 这个容器。 HLS 有"原生解码"和"hls.js 软解"两条路,断一条还有另一条; FLV 只有 flv.js 一条路,断了就彻底没戏。
这是 Apple 的策略选择:HLS 是 Apple 自己发明的协议,内置在 AVFoundation 框架里, 走硬件解码,省电、流畅。Apple 希望 iOS 上的流媒体统一走 HLS。
3.3 除了 iOS,还有哪些环境强制 HLS
packages/player-core/src/env.ts:70:
js
requiresHls = isIOS || isWeChat || isUC || isQuark || !hasMediaSource| 环境 | 为什么 | 跨平台? |
|---|---|---|
| iPhone / iPad | 没有 flv.js 所需的标准 MSE 路径(§3.2) | —— |
| 微信 WebView | 坑 #28:内置播放器劫持视频流 | iOS + Android 都拦 |
| UC 浏览器 | 坑 #29:同上 | iOS + Android 都拦 |
| 夸克浏览器 | 坑 #29:同上 | iOS + Android 都拦 |
| 无 MSE / SSR | 保守兜底 | —— |
注意:微信/UC/夸克的拦截与苹果无关 —— 安卓微信同样被强制 HLS。 原因是这些浏览器会用自己的播放器接管视频流,flv.js 的 MSE 播放会被破坏。
3.4 ⚠️ 常见误解:"分界线是苹果 / 非苹果"
不是。 判定正则是 /iPad|iPhone|iPod/i(env.ts:38),不匹配 Macintosh。
| 设备 | 走什么 |
|---|---|
| macOS Chrome | FLV ✅ |
| macOS Safari | FLV ✅ ← 苹果设备,走 FLV |
| iPhone / iPad | HLS |
真正的分界线是 iOS,不是 Apple。 记成"苹果都走 HLS"会让你在 Mac 上做出错误的性能判断。
3.5 只传 FLV 会怎样
js
source: { url: 'https://cdn/live.flv', type: 'flv', live: true }| 环境 | 结果 |
|---|---|
| 你的开发机(Mac / Windows Chrome) | ✅ 播得好好的 |
| iPhone / 微信 / UC / 夸克 | ❌ 抛 E_MEDIA_NOT_SUPPORTED,黑屏 |
这就是最坑的地方:你本地永远测不出来。
线上不要再从 URL 或用户投诉猜测这一类问题:SDK 会在构造前发送 sourceroute.unsupported,其中只包含 候选类型、live/vod 与闭集 runtime,不含 URL/UA。宿主应把它交给 telemetry;unsupported 说明候选源 不足,不等于播放器已开始播放后失败。具体看板与回滚口径见 支持矩阵与发布运营。
错误信息会明确告诉你怎么修(source-router.ts:79-81):
FLV 在 iOS / 微信 / UC / 夸克 下无法播放,且候选源里没有 HLS 兜底。FLV 请始终和 HLS 一起放进 sources 数组。
而且这个拦截发生在 new Player() 之前(选源是纯函数,内核插件必须在构造时注册), 是同步抛异常,播放器根本不会被创建 —— 不是"试了一下失败",是"根本不试"。
3.6 直播实际走向矩阵(传 [flv, hls] + live: true)
| 环境 | 选中源 | 内核 |
|---|---|---|
| Windows / macOS Chrome | FLV | flv.js |
| macOS Safari | FLV | flv.js |
| Firefox 桌面 | FLV | flv.js |
| Android Chrome | FLV | flv.js |
| iPhone / iPad Safari | HLS | hls.js(经 ManagedMediaSource · ADR-056) |
| iOS 微信 | HLS | hls.js(同上) |
| Android 微信 | HLS | hls.js |
| Android UC / 夸克 | HLS | hls.js |
| SSR(无 window) | HLS | native |
| iOS < 17.1(没有 MMS) | HLS | native ← 现在只剩这一档回落原生 |
一套配置全平台通吃 —— 这就是必须传两种源的全部意义。
四、点播:为什么 HLS 一种就够,以及为什么不推荐 MP4
4.1 为什么 HLS 一种够 —— 因为它有两条播放路径
| 环境 | 内核 | 说明 |
|---|---|---|
| Chrome / Edge / Android | hls.js | MSE 软解 |
| Firefox | hls.js | 坑 #22:Firefox 不原生支持 HLS,必须 hls.js |
| iPhone / iPad / macOS Safari | hls.js | ADR-056:经 ManagedMediaSource(iOS 17.1+)/ 普通 MSE(桌面) |
| 微信 / UC / 夸克 | hls.js | iOS 与 Android 现在一致 |
| iOS < 17.1 · SSR(无 window) | native | 既没有 MSE 也没有 MMS 时才回落 |
⚠️ 「iOS/Safari 走 native」那条规则被 ADR-056 换掉了。 现在的判据是能力不是平台:
if (env.hasMediaSource || env.hasManagedMediaSource) return 'hls.js'。 换来的是 iOS 上的清晰度控制(见 § 4.2 的更正)、以及 #310 / #306 两个缺陷的根治; 代价是 iOS 上的 AirPlay(hls.js 走 MMS 时会置disableRemotePlayback = true)。
HLS 原生和 hls.js 两条路,任意一条通就能播 —— 这是它和 FLV 的根本区别, 也是"一种源就够"的底气所在。
4.2 为什么推荐 HLS 而不是 MP4
MP4 看起来更简单(一个文件、原生播放、不用切片),但点播场景 HLS 有几个 MP4 给不了的能力:
① 多码率自适应(ABR)—— 决定性理由
这是最关键的一条。
| MP4 | HLS | |
|---|---|---|
| 码率 | 单文件单码率 | 一个 m3u8 挂多档(360p/720p/1080p) |
| 弱网表现 | 卡着转圈等,没有退路 | 自动降档到低清,保持流畅不中断 |
| 用户切清晰度 | 换 URL,重新加载、进度重置 | 无缝切换,进度不变 |
而且对本 SDK 来说,这是硬性能力差异:
packages/player-core/src/create-player.ts:227-231 —— 多码率能力完全来自 hls.js 实例:
js
// 多码率能力来自 hls.js:currentLevel = -1 交给 ABR,>=0 锁定该档。
// 单码率源(MP4 / 单档 HLS)拿不到 hls 实例,直接 no-op。
const hls = getHlsInstance(player)
if (!hls) return用 MP4 的直接后果:
setQuality()静默失效(no-op,不报错)ready事件的quality字段为空qualitychange事件永远不触发- 你基于这些做的清晰度菜单 UI 会是空的
✅
同一个限制在 iOS/Safari 上也存在—— 这条已被 ADR-056 解决,别再照着它设计。 那句话的前提是「iOS 走 native 内核,拿不到 hls.js 实例」。ADR-056 把 HLS 内核改成 按能力选之后,iOS 17.1+ 走 hls.js(经 ManagedMediaSource),档位列表读得到。 2026-08-25 真机实测(iPhone 16 Pro / iOS 26.6.1,Safari + Chrome for iOS + 微信三者一致): 多码率源上 SDK 路径档位 = 2,同页面原生对照 = 0。仍然读不到档位的只剩两种:MP4 源(本节主题)和 iOS < 17.1 / SSR(回落 native)。
但下面那条防御式渲染仍然值得照做 —— 档位为空是 MP4 源和老设备上的真实情况。
做清晰度菜单 UI 时必须处理档位为空的情况。参考实现的做法 (
examples/team-video-vue/TeamVideoPlayer.vue:403)是条件渲染:vue<QualityMenu v-if="showQualityMenu && qualityLevels.length >= 2" />阈值取
>= 2而非> 0,和 SDK 侧挂 ABR 监听的条件 (create-player.ts:104)对齐 —— 单档源切换没有意义,不该给用户一个只有一个选项的假菜单。
② 按需分片加载
| MP4 | HLS | |
|---|---|---|
| 加载方式 | 整个文件(靠 HTTP Range 分段) | 天然按切片,只下当前需要的 |
| seek 到未缓冲处 | 依赖服务器正确支持 Range 请求 | 直接请求对应切片 |
| 长视频首帧 | 若 moov 在文件尾(未做 faststart)→ 必须下完整个文件才能起播 | 无此问题 |
那个 moov 的坑很实在:没做 -movflags +faststart 的 MP4,两小时的电影要下完才能播。
③ 直播点播统一
直播已经必须有 HLS 了。点播也用 HLS,后端一套切片流程、CDN 一套缓存策略、前端一套调试经验。 引入 MP4 等于多维护一条链路。
4.3 那 MP4 什么时候用?
MP4 不是不能用,它在这些场景反而更合适:
| 场景 | 为什么 MP4 更好 |
|---|---|
| 短视频(< 2 分钟) | ABR 意义不大,切片反而增加请求数和复杂度 |
| 极致兼容兜底 | MP4 走原生 <video>,几乎不可能失败,适合做最后一道保险 |
| 不想搭切片流程 | 直接扔文件到 OSS 就能用,零转码 |
js
// 点播加 MP4 兜底(可选)
source: {
sources: [
{ url: 'https://cdn/vod.m3u8', type: 'hls' }, // 首选:ABR
{ url: 'https://cdn/vod.mp4', type: 'mp4' }, // 兜底:原生
],
}因为点播优先级是 ['hls', 'mp4', 'flv'],HLS 存在时永远优先,MP4 只在 HLS 缺失时兜底。
注意:MP4 一律走浏览器原生
<video>。xgplayer-mp4插件是永久红线,不引入(坑 #30/#31/#32: Issue #1872 卡死 / #1578 iOS 17+ 不兼容 / #964 SourceBuffer 溢出)。
4.4 澄清:字幕跟容器格式无关
一个容易搞错的点 —— 字幕不是 HLS 的特权。
本 SDK 的字幕走 source.subtitles 独立传入(packages/protocol/src/configs.ts:37-53,ADR-027), 支持 url 和 content 两种模式的 WebVTT / SRT:
js
source: {
url: 'https://cdn/vod.mp4', type: 'mp4', // ← MP4 一样有字幕
subtitles: [
{ mode: 'url', url: 'https://cdn/zh.vtt', locale: 'zh-CN', label: '中文' },
{ mode: 'url', url: 'https://cdn/en.vtt', locale: 'en-US', label: 'English' },
],
}所以"选 HLS 是为了字幕"这个理由不成立。选 HLS 的真正理由是 §4.2 的 ABR 和分片加载。
4.5 点播 ✅ 结论
传 HLS 一种就够,全平台通吃。 推荐 HLS 而非 MP4 的核心理由是多码率 ABR —— 用 MP4 会让 SDK 的清晰度能力整体失效。
五、完整决策流程
mermaid
graph TD
A[传入 source] --> B[normalizeSource<br/>归一成候选列表]
B --> C{requiresHls?<br/>iOS/微信/UC/夸克/无MSE}
C -->|是| D{有 HLS 候选?}
D -->|有| E[选 HLS]
D -->|无,但有 MP4| F[选 MP4 · 原生]
D -->|只有 FLV| G["❌ 抛 E_MEDIA_NOT_SUPPORTED"]
C -->|否| H{live?}
H -->|直播| I["按 flv → hls → mp4 找"]
H -->|点播| J["按 hls → mp4 → flv 找"]
E --> K[pickKernel 选内核]
F --> K
I --> K
J --> K
K --> L{什么类型?}
L -->|mp4| M[native]
L -->|flv| N[flv.js]
L -->|hls| O{有 MSE 或 MMS?}
O -->|是| Q[hls.js · 含 Firefox / iOS 17.1+]
O -->|否| P[native HLS<br/>iOS < 17.1 / 无能力环境]
classDef bad fill:#fdd,stroke:#c00,stroke-width:2px
class G bad四段职责分离:
normalizeSource() → routeSource() → pickKernel() → new Player()
归一化输入 选哪个候选源 决定怎么解 构造时锁定内核六、给后端的要求
6.1 直播流
| 格式 | 必需性 | 服务对象 |
|---|---|---|
| HTTP-FLV | 必需(除非纯内网 PC) | Mac / Windows / Android 普通浏览器 —— 低延迟主力(1-3s) |
| HLS (m3u8) | 必需(除非纯内网 PC) | iOS 全系 + 微信 / UC / 夸克 —— 少了这条 iOS 用户直接报错 |
6.2 点播
| 格式 | 必需性 | 说明 |
|---|---|---|
| HLS (m3u8) | 必需 | 多码率 ABR 靠它;建议至少切 2 档,否则 ABR 无意义 |
| MP4 | 可选 | 极致兼容兜底;做了记得 -movflags +faststart |
6.3 LL-HLS(可选)
想让 iOS 也拿到低延迟,需要上 LL-HLS(EXT-X-PART-INF + HTTP/2 chunked CDN)。
所有浏览器都必须前端显式开启(packages/protocol/src/configs.ts,ADR-024 + ADR-056):
js
source: {
live: true,
sources: [...],
hls: { lowLatencyMode: true }, // ← 所有浏览器都必须显式传
}⚠️ 这里从前写的是「Safari / iOS 走原生,自动检测
EXT-X-PART-INF,无需配置」—— 那句话被 ADR-056 作废了。 内核判据改成能力之后,Safari 和 iOS ≥ 17.1 都走 hls.js, 没有谁再替你自动识别 LL 标签。不显式开的后果是静默的:不报错,延迟退回普通 HLS 水平。唯一还走原生、还能自动检测的是 iOS < 17.1(既无 MSE 也无
ManagedMediaSource), 那一档传不传都对 —— 所以无脑统一传lowLatencyMode: true是跨平台唯一正确的写法。
本地验证源(只验证接线,不替代生产 CDN)
仓库可用下面命令启动带 EXT-X-PART 和阻塞式重载的本地 LL-HLS 源站:
bash
node e2e/manual/ll-hls-server.mjs
# http://localhost:5189/live/ll.m3u8四个组件 Demo 均可用深链验证配置是否被传入:
text
http://localhost:5173/inline?src=http%3A%2F%2Flocalhost%3A5189%2Flive%2Fll.m3u8&type=hls&live=1&lowLatency=1pnpm demo:live 的 5186 是普通滑动 HLS,不带 EXT-X-PART,不应用来证明 LL-HLS 延迟。 本地 5189 源站用于验证 playlist 语义、阻塞重载及前端开关;它不具备生产 CDN 的 HTTP/2 边编码边下发、缓存策略和端到端延迟 SLO,生产验收仍须针对真实打包器与 CDN 测量。
七、其他必须知道的坑
7.1 签名 URL 建议显式传 type
类型推断会先剥掉 query 和 hash 再看扩展名(source-normalize.ts:27,坑 #34), 所以 https://cdn.com/v.m3u8?token=xxx 其实能正确认出。
但多源写法下 type 是 schema 强制必填(configs.ts:77-83:"多源场景下不允许靠后缀猜"), 且无扩展名的 URL(如 /live/stream123)必然推断失败。养成显式传的习惯最稳妥。
推断不出来时会直接抛错,不猜:
无法从 URL 推断媒体类型:xxx。请显式传 type,如 { url, type: 'hls' }
设计取向:与其猜错走到 MP4 路径然后黑屏,不如立刻报错。
7.2 ⚠️ 跨内核换源必须销毁重建
内核插件(hls.js / flv.js)是 new Player() 时注册进去的,运行时换不了 (create-player.ts:278-281):
换源需要从 flv.js 内核切到 hls.js 内核,运行时无法切换内核。请销毁当前 player 并用新的 source 重建。
业务影响:直播频道列表切台时,如果频道 A 走 FLV、频道 B 走 HLS, 不能用 load() 换源,必须销毁播放器重建。同类型换源不受影响。
权益换源同样按重建处理:登录、会员升级、签名续期、地区或内容权限变化时,由宿主向业务服务重新取得 完整签名 MediaSource,再用新的 revision/key 重建播放器。不要由前端拼高清 URL,也不要猜测新源是否仍会 选中同一个内核;点播在新流 ready 后恢复位置,直播回 live edge。无权/下架/地区限制是业务终态,不能重连旧源 或显示成 Loading。公开站点的完整宿主消费流程见鉴权与签名失效。
7.3 ⚠️ HLS 回落 native 时读不到清晰度档位
这不是“iOS / Safari 一律如此”。ADR-056 后,有 MSE 或 ManagedMediaSource 的设备走 hls.js, 多码率 HLS 可以得到档位并切换。只有 HLS 回落到 native 的环境(当前主要是 iOS < 17.1,或没有 MSE/MMS 的能力环境)拿不到 hls.js 实例:ready.quality 为空、setQuality no-op、 qualitychange 不触发。原生 HLS 的 ABR 由浏览器/AVFoundation 接管。
因此清晰度菜单仍必须条件渲染,而不是根据 UA 隐藏;参考实现以 qualityLevels.length >= 2 为条件。这样 MP4、单档 HLS、native HLS 回落都不会出现空菜单。
附:速查卡
js
// ═══ 直播 · 覆盖手机(标准写法)═══
source: {
live: true, // ← 别漏,翻转优先级
sources: [
{ url: 'https://cdn/live.flv', type: 'flv' }, // 桌面/安卓 低延迟 1-3s
{ url: 'https://cdn/live.m3u8', type: 'hls' }, // iOS/微信 兜底(必需)
],
hls: { lowLatencyMode: true }, // 可选,上了 LL-HLS 才传
}
// ═══ 直播 · 纯内网 PC(可简化)═══
source: { url: 'https://cdn/live.flv', type: 'flv', live: true }
// ═══ 点播 · 标准写法 ═══
source: { url: 'https://cdn/vod.m3u8', type: 'hls' }四条铁律:
- 直播必传 FLV + HLS —— 少了 HLS,iOS 用户黑屏,而你本地测不出来
- 点播传 HLS 即可 —— 别用 MP4 当主源,会让清晰度能力整体失效
- 直播别忘
live: true—— 漏了会按点播优先级选 HLS,白白多十几秒延迟 - "只支持桌面"要慎重下结论 —— 链接被转发进微信就破功了