Skip to content

怎么选视频源

本页摘自仓库的视频源配置指南,构建时 @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
监控大屏 / KioskFLV 即可设备固定
不确定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(默认)
首选FLVHLS
核心诉求低延迟多码率 + 可 seek
必传FLV + HLSHLS

⚠️ 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 ChromeFLV
macOS SafariFLV ✅ ← 苹果设备,走 FLV
iPhone / iPadHLS

真正的分界线是 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 ChromeFLVflv.js
macOS SafariFLVflv.js
Firefox 桌面FLVflv.js
Android ChromeFLVflv.js
iPhone / iPad SafariHLShls.js(经 ManagedMediaSource · ADR-056)
iOS 微信HLShls.js(同上)
Android 微信HLShls.js
Android UC / 夸克HLShls.js
SSR(无 window)HLSnative
iOS < 17.1(没有 MMS)HLSnative ← 现在只剩这一档回落原生

一套配置全平台通吃 —— 这就是必须传两种源的全部意义


四、点播:为什么 HLS 一种就够,以及为什么不推荐 MP4

4.1 为什么 HLS 一种够 —— 因为它有两条播放路径

环境内核说明
Chrome / Edge / Androidhls.jsMSE 软解
Firefoxhls.js坑 #22:Firefox 不原生支持 HLS,必须 hls.js
iPhone / iPad / macOS Safarihls.jsADR-056:经 ManagedMediaSource(iOS 17.1+)/ 普通 MSE(桌面)
微信 / UC / 夸克hls.jsiOS 与 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)—— 决定性理由

这是最关键的一条。

MP4HLS
码率单文件单码率一个 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)对齐 —— 单档源切换没有意义,不该给用户一个只有一个选项的假菜单。

② 按需分片加载

MP4HLS
加载方式整个文件(靠 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), 支持 urlcontent 两种模式的 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=1

pnpm demo:live5186 是普通滑动 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' }

四条铁律:

  1. 直播必传 FLV + HLS —— 少了 HLS,iOS 用户黑屏,而你本地测不出来
  2. 点播传 HLS 即可 —— 别用 MP4 当主源,会让清晰度能力整体失效
  3. 直播别忘 live: true —— 漏了会按点播优先级选 HLS,白白多十几秒延迟
  4. "只支持桌面"要慎重下结论 —— 链接被转发进微信就破功了