Appearance
Protocol: Events
所有从 player 上抛到 host(或 inline 场景直接触发)的事件。
核心事件
| Event | Payload | 触发时机 |
|---|---|---|
sourceroute | { outcome, sessionId, candidateTypes, mediaType, kernel, streamKind, runtime, reason, errorCode? } | SourceRouter 的实际选源结论,先于 Player 生命周期。selected 只表示已选中实际媒体类型/内核,不代表首帧或播放成功;unsupported 在既有 E_MEDIA_NOT_SUPPORTED 抛出前发出,mediaType / kernel 为 null,随后错误行为保持不变。runtime 只含闭集 platform/browser/MSE 分桶,候选不含 URL;禁止由 URL、宿主 tag 或 raw UA 反推。React 为 onSourceRoute,Vue 为 sourceRoute / @source-route;遥测推荐消费原始事件流。4.5.0+(ADR-081) |
ready | { duration, quality[], subtitles?[] } | 播放器 metadata 就绪(subtitles 可选,1.2.0+,ADR-027) |
play | {} | 开始播放 |
pause | {} | 暂停 |
ended | {} | 播放结束 |
timeupdate | { time, duration } | 播放进度更新(~250ms;直播 duration 为 0) |
volumechange | { volume, muted } | 音量或静音变化 |
seeking | { time } | 开始 seek |
seeked | { time } | seek 完成 |
waiting | {} | 缓冲开始(卡顿) |
playing | {} | 缓冲结束,继续播放 |
qualitychange | { level, auto } | 清晰度变化(auto=ABR 自动切) |
subtitlechange | { id: number | null } | 字幕轨切换(null=已关闭;无 auto,字幕无 ABR)。1.2.0+(ADR-027) |
error | PlayerError | 错误发生 |
autoplayblocked | {} | 自动播放被拒 |
reconnectstart | { attempt, maxAttempts, reason?: ErrorCode, nextDelayMs? } | 开始重连。reason 是契约错误码,不是自由字符串(ADR-069) |
reconnectsuccess | { attempts } | 重连成功 |
reconnectfailed | { attempts, reason?: ErrorCode } | 重连失败(用满 maxRetries) |
recovery | { recoveryId, phase, strategy, trigger, attempt, maxAttempts, reason?, validatedBy?/outcome? } | 可验证的恢复生命周期。recovered 仅在 validatedBy: 'playing_position_advance' 时出现;failed / cancelled 带登记的 outcome。4.1.0+(ADR-079) |
compatwarning | { code, message, ua, hostVersion?, iframeVersion? } | 兼容性警告。两类来源共用:浏览器内核(五面)/ 契约版本同 major 不同 minor(只 iframe 两面,后两个字段仅这一类携带,ADR-070) |
stalled | { phase, position, durationMs?, kind? } | 测量过的卡顿(HealthMonitor;phase=start/end,end 带 durationMs)。1.1.0+ · kind 4.3.0+(ADR-075):playback / firstframe / seek,start 与 end 带同一个值。⚠️ 只有 playback 该进卡顿率 —— firstframe 是起播等待(它的 KPI 由 firstframe 事件管),seek 是用户自己拖进度条造成的,算进卡顿率等于把用户的操作记成播放器的故障。三类混在一起正是 #391 说的「只有『发生了』没有分布」里缺的那一刀。可选字段,老消费方不改代码也不炸。 |
playablechange | { playable, reason, recoverable } | 聚合播放状态变化 —— 「现在能不能正常出画面」的唯一结论(PlayableStatePlugin)。1.9.0+(ADR-043) |
kernelhealth | { degraded, reason, count, durationMs, detail } | 非致命内核诊断的 5 秒聚合 —— 「为什么卡住」。4.1.0+(ADR-062) |
audiohealth | { degraded, reason: 'audio_data_gap', durationMs } | FLV 音频数据轨不再进入播放器 / 恢复进入的成对信号。它不是内容静音、用户静音或设备音量。4.4.0+(ADR-078) |
bufferhealth | { draining, marginSec } | 缓冲余量在净流失 —— 唯一一条「事情还没坏」的信号。判据是「连续 3 次采样下降」而不是绝对秒数(实测健康余量 3.35~6.40s 与故障那一刻的 6.96s 重叠)。marginSec 可为负,负号是「追不上直播边缘」与「源头挂了」唯一的分叉点。4.1.0+(ADR-071) |
contextchange | { sessionId, kernel, mediaType?, streamKind?, runtime?, position, qualityLevel, srcOrigin, srcPath } | 播放上下文变了 —— 上报适配器要的、消费方在 SDK 外面拿不到的那几格(哪一次播放 / 哪个内核 / 播的哪个候选源 / 当前档位)。新 player 必带实际 mediaType / streamKind / 闭集 runtime,但三者为 optional:同 major 的旧 iframe producer 缺失时只能按 unknown 处理,不能从 URL 猜测。只在结论变化时发;position 不参与触发判据(每 250ms 都在变,进去就等于复制一条 timeupdate),要「出错那一刻」的位置请在 error 回调里同步调 getPlaybackContext()。源地址只给脱敏后的 origin + pathname,签名参数全在 query 里(ADR-022/081)。4.2.0+(ADR-074; 新维度 4.5.0+) |
firstframe | { fvt } | 首帧可见(毫秒)。来自 xgplayer 的 XGLogger —— 它一直在算,而 SDK 从没读过(ADR-075)。⚠️ 和 pnpm test:e2e:kpi(#148)量的不是一件事:那条是端到端含页面加载,这条是播放器内部口径,两者不可互换。4.3.0+(ADR-075) |
framefreeze | { durationMs, droppedFrames, totalFrames } | 画面冻结 —— 缓冲是够的,但解码器出不了新帧。来自 FpsDetect,判据是「连续 3 个 tick 解码帧数 ≤ 0 且缓冲够、没暂停、页面没隐藏」。⚠️ 不是掉帧率(droppedFrames 是附带数据不是触发原因),也不与 stalled 重复(后者是缓冲驱动的等待,这条的前提恰恰是缓冲够,指向解码侧)。⚠️ 仅 PC —— 上游按 sniffer.device 分档装载,手机上结构性不触发;这是平台轴差异不是模式轴,五种接入方式在同一台设备上一致(ADR-075 决策⑤,已登记上行缺口)。4.3.0+ |
useraction | { action, source, from, to } | 用户动作,经白名单过滤,只含发生在 SDK 内部的那些(全屏 / 清晰度 / 倍速 / 音量 / 静音 / 画中画 / seek / 旋转 / 截图 / 下载 / 弹幕 / 错误重试 / 播放暂停)。契约里已有 play / volumechange / seeking / qualitychange,而它们回答不了「是谁发起的」 —— 用户点的还是代码调的,本事件补的就是这一格(ADR-039 零消费判据)。裸 DOM 点击 / 拖动不进:消费方在自己容器上监听就有,而拖动还会再发一条 seek。⚠️ 传 controls: false 时绝大多数动作不再发出(发出方是播放器自己的控件)。4.3.0+ |
事件名全小写连写(对齐 HTML5 媒体事件),是 wire 上的名字;消费面各自映射 ——Vue emit
time-update,React proponTimeUpdate。
reconnect*.reason 是 ErrorCode,服务端可以直接聚合(ADR-069)
它的取值就是 errors.md 那张表里的码;缺省 = 手动 reconnect()(没有触发它的错误)。
别照着「今天实际只会出现哪几个码」去收窄。 那要复刻 ReconnectPlugin 的两道过滤 (retryable === true 且 category !== 'media'),而那两道闸是实现细节 —— 改一行就和契约对不上了。
⚠️ 这个字段此前的类型是
string。 后果不是「类型不精确」而已: 收紧当天,全仓三处 fixture 当场编不过,写的都是 SDK 从来不产的值 ('network'/'timeout'/'stalled'—— 最后那个还是个事件名)。 它们此前一直是绿的。
stalledvswaiting/playing:后两者是 HTML5 裸事件透传(只有开始/结束瞬间、 不带测量、测不到冻帧型隐性卡顿);stalled是 HealthMonitor 测量后的结果,phase='end'带durationMs供埋点算卡顿率。冻结后 1.1.0 minor 新增(ADR-026)。无条件 emit —— 早期的consent.analytics门控已按 ADR-040 移除(它会在冻帧时掐掉唯一的 UI 信号),合规过滤归消费方的上报层。
recovery 是恢复成功的可验证证据(ADR-079)
reconnectsuccess 保持原来的 UI 兼容语义,不能单独作为生产上报里的“恢复成功”。需要归因或 QoE 时订阅原始 recovery:同一 recoveryId 依次给出检测、尝试、验证与唯一终态。只有在恢复动作 之后观察到 playing,再观察到后续位置推进,才会发出:
ts
{
event: 'recovery',
payload: {
recoveryId: 42,
phase: 'recovered',
strategy: 'reconnect',
trigger: 'error',
attempt: 1,
maxAttempts: 3,
validatedBy: 'playing_position_advance',
},
}failed 只会使用 timeout / attempts_exhausted;cancelled 只会使用 source_changed / destroyed / superseded。SDK 不发“半成功”,消费方也不必为这条 lifecycle 自己再起超时。
playablechange不是又一个原始信号,是上面那一堆的唯一结论。消费方写if (!playable) 盖上自己的 loading就覆盖了全部「播不了」的成因,不用自己拼装ready/waiting/stalled/reconnect*/autoplayblocked/error/onFallback再兜一个握手超时。 与stalled的分工:stalled是测量结果(带durationMs,给埋点),playablechange是实时态(给 UI);前者是后者的数据源之一,不是替代。只在结论变化时发。
reason取值,优先级从高到低(同时命中报最严重的那个):error→frame_disconnected→autoplay_blocked→reconnecting→stalled→buffering→initializing→degraded→ok。degraded(掉帧严重)是唯一playable仍为true的非ok值 —— 画面还在动、 只是质量在掉,消费方可以忽略。recoverable决定盖 loading(等)还是露重试入口(不等)。
frame_disconnected是唯一由宿主侧(frame-core)本地产生的 reason —— 「iframe 还没起来」 这件事 iframe 自己没法报,inline 模式则压根没有这个阶段、永不出现。它也是唯一recoverable随阶段变化的一支:握手中true(正常启动,该等),握手失败降级后false(该露重试入口)。 静态 iframe 模式没有这一条 —— 那条路的 iframe 是消费方自己建的,embed-helper只监听 postMessage,看不见 iframe 的加载状态。
kernelhealth补的是成因那一格。stalled有时长没成因、playablechange有结论没成因, 而最典型的直播故障(断流)下内核根本不升 fatal、error一条都不发 —— 实测:注入立即断流后 60 秒内 hls.js 报了 10,137 条非致命诊断,消费方拿到 0 条error(ADR-062)。聚合,不是转发。 固定 5000ms 窗口,窗口内不论内核报多少条最多产出一条(169 条/秒 → 0.2 条/秒)。 窗口写死在 SDK 里、不进配置(ADR-029:一旦可配,「调到 100ms 又把消费方打死」就成了 SDK 的锅)。
两个状态:
degraded: true= 本窗口有诊断;degraded: false= 安静了一整个窗口,是收尾那条。 收尾那条是硬要求 —— 没有它,消费方判断「内核安静了」只能自己兜超时,而那正是 ADR-043 消灭过的东西。
reason三档(network/media/other)的判据是「会不会让消费方做不同的事」: network 提示检查网络、media 换清晰度或换设备、other 只能上报。归一复用error-mapping.ts那份类型表,error和kernelhealth两条通道共用一个真相。manifest归other不是笔误:manifest 解析坏了重试和换清晰度都没用(加载失败走networkError→network)。detail是内核原文,只给日志看,不许switch—— 它跟着 hls.js 版本走,不是契约的一部分。⚠️ 拿它做上报和排障,不要拿它驱动 UI:首条最迟 5 秒后到。实时 UI 归
playablechange。⚠️ FLV 源永远不发这个事件。 flv.js 没有 fatal / 非 fatal 之分,每条
ERROR都是终局、 已经走了error流。FLV 侧没有「非致命诊断」可聚合 —— 已知边界,不是漏实现(ADR-062 ④)。
subtitlechange+setSubtitle+ready.subtitles是字幕控制侧闭环(冻结后 1.2.0 minor, ADR-027),镜像 quality 的qualitychange/setQuality/ready.quality。字幕菜单 UI 归团队层 (同QualityMenu),SDK 只暴露轨道 + 切换命令 + 渲染引擎(xgplayer TextTrack,isShowIcon:false)。
详细说明
权威来源:packages/protocol/src/events.ts 的 Zod schema。