Skip to content

Protocol: Events

所有从 player 上抛到 host(或 inline 场景直接触发)的事件。

核心事件

EventPayload触发时机
sourceroute{ outcome, sessionId, candidateTypes, mediaType, kernel, streamKind, runtime, reason, errorCode? }SourceRouter 的实际选源结论,先于 Player 生命周期selected 只表示已选中实际媒体类型/内核,不代表首帧或播放成功;unsupported 在既有 E_MEDIA_NOT_SUPPORTED 抛出前发出,mediaType / kernelnull,随后错误行为保持不变。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)
errorPlayerError错误发生
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,enddurationMs)。1.1.0+ · kind 4.3.0+(ADR-075):playback / firstframe / seek,startend 带同一个值。⚠️ 只有 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 prop onTimeUpdate

reconnect*.reasonErrorCode,服务端可以直接聚合(ADR-069)

它的取值就是 errors.md 那张表里的码;缺省 = 手动 reconnect()(没有触发它的错误)。

别照着「今天实际只会出现哪几个码」去收窄。 那要复刻 ReconnectPlugin 的两道过滤 (retryable === truecategory !== 'media'),而那两道闸是实现细节 —— 改一行就和契约对不上了。

⚠️ 这个字段此前的类型是 string 后果不是「类型不精确」而已: 收紧当天,全仓三处 fixture 当场编不过,写的都是 SDK 从来不产的值 ('network' / 'timeout' / 'stalled' —— 最后那个还是个事件名)。 它们此前一直是绿的。

stalled vs waiting/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_exhaustedcancelled 只会使用 source_changed / destroyed / superseded。SDK 不发“半成功”,消费方也不必为这条 lifecycle 自己再起超时。

playablechange 不是又一个原始信号,是上面那一堆的唯一结论。消费方写 if (!playable) 盖上自己的 loading 就覆盖了全部「播不了」的成因,不用自己拼装 ready/waiting/stalled/reconnect*/autoplayblocked/error/onFallback 再兜一个握手超时。 与 stalled 的分工:stalled测量结果(带 durationMs,给埋点),playablechange实时态(给 UI);前者是后者的数据源之一,不是替代。只在结论变化时发。

reason 取值,优先级从高到低(同时命中报最严重的那个): errorframe_disconnectedautoplay_blockedreconnectingstalledbufferinginitializingdegradedokdegraded(掉帧严重)是唯一 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 有结论没成因, 而最典型的直播故障(断流)下内核根本不升 fatalerror 一条都不发 —— 实测:注入立即断流后 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 那份类型表,errorkernelhealth 两条通道共用一个真相manifestother 不是笔误:manifest 解析坏了重试和换清晰度都没用(加载失败走 networkErrornetwork)。 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。