Shaka Player MoQ 实战指南:基于 MSF 解析器的 Media over QUIC 直播流接入
【免费下载链接】shaka-playerJavaScript player library / DASH & HLS client / MSE-EME player项目地址: https://gitcode.com/GitHub_Trending/sh/shaka-player
本篇指南讲解如何在 Shaka Player 中使用内置的MSF(MoQ Streaming Format)清单解析器,通过MoQT(Media over QUIC Transport)协议拉取并播放实时直播内容。文章覆盖从环境前置条件、player.load()接入方式,到目录(Catalog)发现机制、四类媒体打包格式、manifest.msf全量配置项、DRM 集成以及本地 relay 联调的完整链路,并深入解析器源码与构建目标,帮助你独立接入 MoQ 直播源。
重要提示:Shaka Player 对 MoQ 的支持目前仍处于experimental(实验性)阶段,仅在实验性构建产物中可用。在底层规范(下文的三个 IETF draft)正式定稿、脱离草稿状态之前,该能力将一直保持实验性质。
相关规范:
- draft-ietf-moq-transport
- draft-ietf-moq-msf
- draft-ietf-moq-cmsf
前置条件:WebTransport 与运行环境
MoQ 流式传输完全依赖浏览器的WebTransport API,因此接入前必须满足以下三点:
- 页面必须通过 HTTPS 或
localhost提供服务——WebTransport 在非安全上下文中不可用; - 浏览器必须支持 WebTransport API(现代 Chrome/Edge 系浏览器已内置);
- MoQT relay/服务端也必须通过 HTTPS 可达——本地联调时可用自签名证书配合
fingerprintUri(下文详述)绕过浏览器校验。
关于第 2 点,Shaka 在解析器启动时会对 WebTransport 可用性做显式校验:相关错误码定义在 lib/util/error.js,其中WEBTRANSPORT_NOT_AVAILABLE(4056)表示浏览器不支持该 API,WEBTRANSPORT_INITIALIZATION_FAILED(4057)表示连接建立失败。
基本用法:mimeType是唯一强制项
加载 MoQ 流的 API 与普通 DASH/HLS 完全一致,但存在一个强制性要求:调用player.load()时,必须将'application/msf'作为mimeType参数传入。这是 Shaka 选择 MSF 清单解析器(而非 DASH/HLS 解析器)的唯一判定依据。
const manifestUri = 'https://relay.example.com/moq-endpoint'; async function initPlayer() { shaka.polyfill.installAll(); if (!shaka.Player.isBrowserSupported()) { console.error('Browser not supported!'); return; } const video = document.getElementById('video'); const player = new shaka.Player(); await player.attach(video); player.addEventListener('error', (event) => { console.error('Error code', event.detail.code, event.detail); }); try { // The mimeType 'application/msf' is REQUIRED for MOQ streams. await player.load(manifestUri, /* startTime= */ null, 'application/msf'); console.log('MOQ stream loaded!'); } catch (e) { console.error('Load failed', e); } } document.addEventListener('DOMContentLoaded', initPlayer);切勿省略
'application/msf'。缺少该 MIME 类型时,Shaka 无法识别这是一个 MoQ 源,要么直接加载失败,要么将其误判为其他格式进行解析。
从源码角度印证:MSF 解析器的注册入口位于 lib/msf/msf_parser.js,即shaka.media.ManifestParser.registerParserByMimeType('application/msf', () => new shaka.msf.MSFParser())。也就是说,'application/msf'与解析器之间是一一映射的关系,这正是该参数不能省略的根本原因。仓库自带的演示应用也在 demo/common/assets.js 中为多个 MoQ 演示资产统一设置了application/msf的 mimeType。
背后发生了什么:从 WebTransport 到媒体管道
当以'application/msf'调用player.load()时,Shaka 会依次执行以下流程(对应MSFParser.start()的实现,见 lib/msf/msf_parser.js):
- 建立 WebTransport 连接:解析器创建
MSFTransport实例并调用connect(uri, fingerprint, authorizationToken)打开到目标 URI 的 WebTransport 通道; - 执行 MoQT 会话建立:完成客户端/服务端握手与 draft 版本协商(协商细节见下文
version配置); - 发现并获取 catalog:若配置了
manifest.msf.namespaces,直接向该命名空间订阅(SUBSCRIBE)或一次性获取(FETCH)catalog 轨道;否则注册PUBLISH_NAMESPACE公告监听(listenForAnnouncements_(),见 lib/msf/msf_parser.js),等待服务端广播命名空间后再动态发现; - 解析 catalog:catalog 本质是一份JSON 文档(结构定义见 externs/msf_catalog.js),解析后枚举出全部音频、视频与文本轨道;
- 订阅轨道数据流:对每个轨道发起 MoQT 数据订阅,把到达的媒体段送入 Shaka 常规媒体管道(MediaSource + SourceBuffer)播放。
需要特别注意的是,catalog 的获取有10 秒超时限制:超过时限仍未拿到 catalog 会触发MSF_CATALOG_TIMEOUT(4064)错误;而 catalog 中无任何可用轨道时则触发MSF_NO_CATALOG(4062)。两者均在 lib/util/error.js 中定义。
仅支持直播(Live):catalog 中
isLive为false的 VOD 内容不受支持。解析器在processCatalog_()之后显式检查presentationTimeline.isLive(),若不满足直接抛出MSF_VOD_CONTENT_NOT_SUPPORTED(4058)错误(见 lib/msf/msf_parser.js)。
另外值得一提的实现细节:MoQ 的媒体按“对象(object)”粒度以小体积 chunk 到达,单个对象往往太小,无法为 ABR 带宽估计器提供有效采样,因此解析器按Group 聚合后上报一次带宽样本,并采用各对象的“有效读取时长”而非墙钟时间——这保证在链路带宽富余时 ABR 能感知到额外容量,而非始终只测出当前档位的码率(见 lib/msf/msf_parser.js 附近注释)。
支持的打包格式(Packaging)
catalog 中的每条轨道通过packaging字段声明其媒体在 MoQT 对象中的封装方式。对于 Shaka 不支持的打包格式,该轨道会被跳过,catalog 中的其余轨道仍可正常播放。
packaging | 对象中包含的内容 | 规范 |
|---|---|---|
cmaf | 一个 CMAF chunk | draft-ietf-moq-cmsf |
chunk-per-object | 一个 CMAF chunk | draft-ietf-moq-cmsf |
loc | 原始码流的一帧 | draft-ietf-moq-loc |
m2ts | 一整段连续的 TS 传输包 | draft-gregoire-moq-msfts |
各打包格式的实现位于 lib/msf/packaging/ 目录(cmaf.js、loc.js、m2ts.js),并通过 打包注册表 以名称注册;应用也可以通过PackagingRegistry.registerPackaging(name, factory)注册自定义打包格式。
MPEG-2 传输流(m2ts)
m2ts打包下,一个 MoQT 对象是一段连续且完整的 TS 传输包。两种源包大小都受支持:188 字节的标准 TS 传输包,以及192 字节的 M2TS 源包(其 4 字节到达时间戳会被丢弃,因为该时间戳与呈现时间无关)。此项判断逻辑在 lib/msf/packaging/m2ts.js,声明了其他包大小(如 204 字节)的轨道会被跳过。
使用m2ts打包有两点必须了解:
轨道必须声明
codec:codec 决定 MediaSource 打开哪些 source buffer,而这一动作发生在第一个 group 到达并可被探测之前,因此无法从媒体数据中推断。若轨道是复用(muxed)节目,需以逗号分隔列出两个 codec,写法与 HLS 的CODECS属性完全一致:{ "name": "program-1-ts", "packaging": "m2ts", "codec": "avc1.64001f,mp4a.40.2", "m2tsPacketSize": 188, "m2tsPcrPid": 257 }对于上面的复用节目,Shaka 会从这一条轨道同时打开一个音频和一个视频 source buffer,并从这个流的 segments 同时喂给两者。源码中该逻辑位于 lib/msf/packaging/m2ts.js:
describeTrack()会把video/mp2t; codecs="avc1.64001f,mp4a.40.2"交给SegmentUtils.getBasicInfoFromMimeType()识别,MediaSourceEngine 识别出该组合后为音视频各开一个 buffer。同时,未声明 codec 或 codec 无法识别的轨道会给出警告并被跳过。延迟等于一个 Group:TS 传输包自身不携带时序,一个 PES 包横跨多个传输包,而对象边界完全由发布方决定切在哪里,因此单个对象无法独立追加。规范能保证的只是“Group 起始于随机访问点”,所以 Group 是最小可追加单元,且只有当下一个 Group 开始时才算完整。相比之下,
chunk-per-object的每个对象到达即可立即追加,因此m2ts的延迟天然更高。相关 catalog 字段还包含
m2tsPacketsPerObject、m2tsProgramNumber、m2tsPmtPid、m2tsPsiInterval、m2tsRandomAccess、m2tsTimestampMode、m2tsScte35Pid等(完整列表见 externs/msf_catalog.js)。当轨道声明m2tsRandomAccess: false时,Shaka 会警告 Group 起始点可能不是随机访问点,开头可能无法解码。
建议声明initData(base64 编码的 PAT/PMT 包):Shaka 会把其中内容规整化为 188 字节传输包并前置拼接到每一个 Group,从而保证当节目的 PSI 信息没有在每个 Group 开头重复时,流仍然可播(见 lib/msf/packaging/m2ts.js 与normalizePackets_实现)。注意 TS 没有传统意义上的初始化段,PAT/PMT 是通过前置到 Group 数据而非单独追加来发挥作用的。
如果发布方在两个 Group 之间发出 PCR 不连续信号,Shaka 会重新锚定媒体时间,使呈现时间持续向前推进,并通知 transmuxer 开启一个新的初始化段。
注意:
m2ts依赖 transmuxer,而 transmuxer 是独立的构建目标。自定义构建时必须把+@transmuxer与+@msf一起包含(构建目标定义见 build/types/msf 与 build/types/complete)。这与 HLS 中传输流分段(TS segments)的情形相同——两者共用lib/transmuxer/下的 TS transmuxer 实现。
MSF 配置详解
所有 MoQ 专属配置均位于manifest.msf之下(各配置项的默认值定义在 lib/util/player_configuration.js):
player.configure({ manifest: { msf: { // Options described below } } });fingerprintUri(string,默认:'')
指向一个纯文本文件的 URL,内容为服务端自签名 TLS 证书的 SHA-256 十六进制指纹。当连接本地 relay 或使用浏览器默认会拒绝的自签名证书服务端时,需要此项。
player.configure({ manifest: { msf: { fingerprintUri: 'https://relay.example.com/cert.hex', } } });设置后,Shaka 会在打开 WebTransport 连接之前先获取该指纹,并用它对证书做固定(pin)。CA 签名证书的服务端留空即可。
源码层面的流程:MSFParser.start()在connect()前检查fingerprintUri,以RequestType.FINGERPRINT请求类型通过 NetworkingEngine 拉取文本,去除空白后逐字节解析为十六进制字节数组,再作为fingerprint参数传入msfTransport_.connect()(见 lib/msf/msf_parser.js)。
namespaces(Array<string>,默认:[])
要订阅 catalog 轨道的 MoQT命名空间。命名空间是字符串路径组件的数组,共同标识 relay 上的会话。
player.configure({ manifest: { msf: { // Subscribe to the catalog in namespace ['live', 'channel1'] namespaces: ['live', 'channel1'], } } });- 设置
namespaces时:Shaka 立即在该命名空间订阅(或获取)catalog; - 留空
[]时:Shaka 改为监听服务端的PUBLISH_NAMESPACE公告,自动采用公告的命名空间(对应listenForAnnouncements_(),见 lib/msf/msf_parser.js)。
若你提前知道命名空间,请使用显式形式,以减少启动延迟(省去等待公告的往返时间)。注意命名空间去重逻辑:同一个 namespace 只会被处理一次(lib/msf/msf_parser.js)。
authorizationToken(string,默认:'')
可选授权令牌,在 MoQT 客户端 setup 握手期间发送给服务端。令牌按规范以别名类型USE_VALUE(0x03)编码。
player.configure({ manifest: { msf: { authorizationToken: 'Bearer my-secret-token', } } });该令牌与fingerprintUri一样,会随connect()一并传入MSFTransport,用于服务端对客户端的鉴权。
subscribeFilterType(MsfFilterType,默认:LARGEST_OBJECT)
控制订阅轨道时应用的过滤器,对应 MoQT 订阅的 filter 参数。枚举定义见 lib/config/msf_filter_type.js:
| 值 | 说明 |
|---|---|
shaka.config.MsfFilterType.LARGEST_OBJECT | 从最新可用的对象开始(默认) |
shaka.config.MsfFilterType.NEXT_GROUP_START | 从下一个可用的 Group 开始 |
player.configure({ manifest: { msf: { subscribeFilterType: shaka.config.MsfFilterType.LARGEST_OBJECT, } } });枚举中还定义了
NONE(0x0)、ABSOLUTE_START(0x3)、ABSOLUTE_RANGE(0x4)等其他取值。订阅请求组装时该值会被序列化进 subscribe 参数(见 lib/msf/request_id_session.js),其中LARGEST_OBJECT对应参数类型0x09(见 lib/msf/msf_control_stream.js)。在直播低延迟场景下,LARGEST_OBJECT意味着从服务端当前已发布的最新对象起播,可最大程度贴近“直播边缘”。
useFetchCatalog(boolean,默认:false)
为true时,Shaka 使用FETCH(一次性检索)而非持续的SUBSCRIBE获取 catalog。适用于 catalog 静态、在会话生命周期内不更新的场景。
player.configure({ manifest: { msf: { useFetchCatalog: true, } } });为false(默认)时,Shaka 订阅 catalog 轨道,服务端若推送 catalog 更新则会被拾取。源码中getCatalog_()据此分流到fetchCatalog_()或subscribeToCatalog_()(见 lib/msf/msf_parser.js)。两种路径都会跳过无负载的对象(这类对象仅携带对象状态标记,如 draft-16 的组结束标记,不包含 catalog 数据)。此外,catalog 结构支持deltaUpdate、addTracks、removeTracks、cloneTracks等增量更新字段(见 externs/msf_catalog.js),为动态更新预留了扩展空间。
version(MsfVersion,默认:AUTO)
控制与 MoQT 服务端协商的 draft 版本。枚举定义见 lib/config/msf_version.js:
| 值 | 提供的 WebTransport 协议串 | 说明 |
|---|---|---|
shaka.config.MsfVersion.AUTO | moqt-18,moqt-16,moq-00 | 提供全部受支持的 draft,最新的优先(默认) |
shaka.config.MsfVersion.DRAFT_18 | moqt-18 | 仅强制 draft-18 |
shaka.config.MsfVersion.DRAFT_16 | moqt-16 | 仅强制 draft-16 |
shaka.config.MsfVersion.DRAFT_14 | moq-00 | 已弃用。仅强制 draft-14;将于 v6 移除 |
player.configure({ manifest: { msf: { version: shaka.config.MsfVersion.DRAFT_18, } } });- Draft-14 已弃用,并将在 v6 中移除。无论是显式选择,还是
AUTO模式下服务端恰好选择了moq-00,都会打印弃用警告。draft-14 早于 draft-15 引入的基于子协议的版本协商机制,它在带内协商,在CLIENT_SETUP中提供版本列表。请在 v6 之前迁移到 draft-16 或 draft-18(README 的能力清单同样标注了 draft-14 已弃用)。 - draft-16 与 draft-18 是两种不同的线上协议,而非同一协议的版本修订:draft-17 替换了变长整数编码、把控制面从单条双向流改为一对单向流、为每个请求分配独立双向流,并重新分配了若干消息类型 ID。因此 Shaka 为每个 draft 维护独立实现,封装为 dialect(方言),在协商阶段选定一次后启用。相关实现见 lib/msf/drafts/ 目录下的
draft14/、draft16/、draft18/,其类型定义见 externs/shaka/msf_dialect.js,并有对应的单元测试 test/msf/dialect_registry_unit.js 与 test/msf/draft18_session_unit.js。
版本通过WebTransport 子协议协商。注意:Shaka 并不要求服务端回显该子协议——部分 relay 会接受客户端提供的子协议,但WebTransport.protocol属性保持为空;若把这种情况视为失败,会破坏本可正常工作的连接。
catalogPreprocessor(function,默认:恒等函数)
可选回调,在 catalog JSON 解析完成后、Shaka 处理其轨道之前被调用,用于以编程方式修改或过滤 catalog 条目。
player.configure({ manifest: { msf: { catalogPreprocessor: (catalog) => { // Example: remove loc tracks from the catalog catalog.tracks = catalog.tracks.filter( (t) => t.packaging !== 'loc'); return catalog; }, } } });该函数接收且必须返回一个msfCatalog.Catalog对象(类型定义见 externs/msf_catalog.js)。默认值为恒等函数(catalog) => catalog。典型用途包括:按业务规则剔除某些包装格式/码率档位的轨道、批量改写轨道元数据等。
完整配置示例
player.configure({ manifest: { msf: { fingerprintUri: '', // Set for self-signed cert servers namespaces: ['live', 'ch1'], // Known namespace; leave [] to auto-discover authorizationToken: '', // Bearer token if required by server useFetchCatalog: false, // true = one-shot FETCH, false = SUBSCRIBE version: shaka.config.MsfVersion.AUTO, // Version negotiation strategy subscribeFilterType: shaka.config.MsfFilterType.LARGEST_OBJECT, catalogPreprocessor: (catalog) => catalog, // Identity (no-op) } } });结合 DRM 的 MoQ 接入
MoQ 流的 DRM 配置方式与 DASH/HLS完全一致。DRM 信息通过 catalog 中的contentProtections条目携带(包含 key system UUID、PSSH 与 license server URL),Shaka 会自动提取并填充其 DRM 子系统。catalog 中contentProtections的 JSON 结构包含refID、defaultKID、scheme与drmSystem(后者含systemID、laURL、certURL、authzURL、pssh、robustness字段),定义见 externs/msf_catalog.js;轨道通过contentProtectionRefIDs引用这些条目(externs/msf_catalog.js),解析器侧的处理在 lib/msf/msf_parser.js(含DrmUtils.getUuidMap()做 key system 识别、PlayReady 特判、defaultKID 提取)。
只有当 catalog 未包含 license server URL,或需要硬件健壮性、自定义 header 等高级选项时,才需要补充 DRM 配置:
player.configure({ drm: { servers: { 'com.widevine.alpha': 'https://license.example.com/widevine', 'com.microsoft.playready': 'https://license.example.com/playready', }, advanced: { 'com.widevine.alpha': { videoRobustness: ['HW_SECURE_ALL'], audioRobustness: ['SW_SECURE_CRYPTO'], } } }, manifest: { msf: { namespaces: ['live', 'encrypted-channel'], authorizationToken: 'my-token', } } }); await player.load(uri, null, 'application/msf');使用本地 Relay 联调测试
当本地 MoQT relay 使用自签名 TLS 证书时,借助fingerprintUri即可完成联调:
- 生成自签名证书,并将其 SHA-256 指纹导出为十六进制字符串(不要冒号、不要空格),写入纯文本文件,如
cert.hex; - 通过 HTTPS 端点向浏览器提供该文件;
- 配置 Shaka:
player.configure({ manifest: { msf: { fingerprintUri: 'https://localhost:4443/cert.hex', namespaces: ['test'], } } }); await player.load('https://localhost:4433/moq', null, 'application/msf');构建说明与实验性限制小结
接入 MoQ 前请确认以下几点与当前仓库能力一致:
- 构建目标:MSF 解析器位于独立构建目标
@msf(build/types/msf),使用m2ts打包还需+@transmuxer(build/types/complete 已默认包含);自定义构建务必同时引入。完整构建目标索引见 build/types/。 - 实验性状态:MoQ 支持目前仅在实验性构建中可用,且将随相关 draft 的演进而变化,生产环境接入前请锁定 Shaka 版本并关注 CHANGELOG.md 与 roadmap.md 中的更新。
- 仅直播:VOD(catalog
isLive: false)会直接报错(MSF_VOD_CONTENT_NOT_SUPPORTED)。 - 安全上下文:页面与 relay 都必须运行于 HTTPS 或
localhost。
完成上述配置后,player.load(uri, null, 'application/msf')即会把一条 MoQT 直播流无缝接入 Shaka 常规的流媒体管道,复用其 ABR、DRM、字幕等既有能力。
【免费下载链接】shaka-playerJavaScript player library / DASH & HLS client / MSE-EME player项目地址: https://gitcode.com/GitHub_Trending/sh/shaka-player
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考