Skip to content

Protocol: Errors

所有错误码定义。命名规则:E_<CATEGORY>_<SPECIFIC>W_<CATEGORY>_<SPECIFIC>(警告)。

全部错误码(17 个)

权威定义见 packages/protocol/src/errors.tsERROR_META;category / retryable 一律由该表推导,不接受调用方传入。

每个码都有真实发射点(ADR-034)——contract-coverage.test.ts 静态扫描全仓库强制这条。 换句话说:这张表里的每一行,你写的分支都真的会被执行到。

播放期(player-core):

CodeCategoryRetryable说明
E_MANIFEST_PARSEmanifestmanifest 解析失败
E_NETWORKnetwork网络错误
E_NETWORK_TIMEOUTnetwork网络超时
E_AUTH_EXPIREDauth401/403,签名 URL 过期或无权限(不做刷新,ADR-022)
E_AUTOPLAY_BLOCKEDautoplay浏览器拒绝自动播放
E_MEDIA_NOT_SUPPORTEDmedia浏览器不支持此格式
E_MEDIA_DECODEmedia解码错误
E_MEDIA_ABORTEDmedia播放被中止
E_SUBTITLE_LOAD_FAILEDmedia字幕轨加载失败(不中断播放)
E_DANMAKU_SEND_FAILEDenv弹幕发送失败(轨道满 / 参数不合法)
E_METHOD_NOT_SUPPORTEDenv命令当前平台不支持(如运行时换内核)
E_PLAYER_DESTROYEDenv命令打进了已销毁的播放器 —— 消费方自己调过 destroy()(ADR-067)
E_INTERNALenvSDK 内部错误
E_UNKNOWNenv其他

iframe 传输层(frame-core):

CodeCategoryRetryable对应 FallbackReason
E_LOAD_FAILEDnetworkiframe_load_failed
E_ENV_CSP_BLOCKEDenvcsp_denied
E_HANDSHAKE_TIMEOUTnetworkhandshake_timeout
E_HANDSHAKE_VERSION_MISMATCHenvversion_incompatible
E_FRAME_CRASHEDenv(无 —— 它不是加载期失败)

iframe 降级走两条通道:onFallback(reason) 驱动兜底 UI,error 事件进监控流。 前四个一一对应,见上表。

E_PLAYER_DESTROYED:唯一一个「消费方自己造成」的码

其余所有码描述的都是外部出了事(网络、媒体、环境、对端)。这一个不是: 只有消费方自己调过 destroy() 才到得了它。组件卸载走的是另一条路 —— 那时句柄已置 null,根本不下发命令(ADR-066)。

它填的是一个静默洞。 player-core 的每个命令开头都有 if (destroyed) return (坑 #3:destroy 之后不许再有事件出去),那个 return 是对的;错的是上一层从来不说话。 实测:消费方调完 destroy() 再改 source prop —— inline 两面 0 条事件, iframe 两面报 E_INTERNAL + 一句 penpal 的英文内部话。两半各错各的, 逐条数据在 archive/docs/specs/destroyed-player-command-signal.md § 1

⚠️ 不要用 E_METHOD_NOT_SUPPORTED 代替它 —— 那是「这个方法在这里不支持」。 合成一个,监控里就再也分不开「调了个不支持的东西」和「你自己把它拆了」, 和下面 E_FRAME_CRASHED 拒绝并进 E_HANDSHAKE_TIMEOUT 是同一条理由。

静态 iframe 结构性没有这个码:embed-helper 的导出只有 on / off / destroy, 没有命令面(命令走 URL 参数),不存在「prop 变了 → 下发命令」这条路。

E_FRAME_CRASHEDE_HANDSHAKE_TIMEOUT 的分别

是运维含义的分别,不是时序细节:

什么时候典型成因
E_HANDSHAKE_TIMEOUT握手从未成功CDN 挂了 / URL 错了 / 版本目录不存在 / CSP
E_FRAME_CRASHED握手成功之后失联渲染进程崩溃 / OOM / iframe 被浏览器回收

合并成一个码,监控里就再也分不开这两类故障 —— 一个是「发布/配置出问题」, 一个是「运行时资源出问题」,处置动作完全不同。

E_FRAME_CRASHED 前面没有 FallbackReason:onFallback 服务的是加载期兜底 UI, 而崩溃发生在播放中。它伴随的信号是 playablechange { reason: 'frame_disconnected', recoverable: false } —— 那才是给 UI 的「露重试入口」结论(ADR-043 / ADR-054)。

平台差异(已在 e2e/parity/ 固化,不是漏实现): inline 永不产生(同上下文,崩了整页一起崩);静态 iframe 结构性没有(单向 postMessage, 宿主侧没有 SDK 代码去检测)。

::: 已删除的错误码 ADR-034 删掉了 12 个从未发射的码:E_MANIFEST_FETCH E_MANIFEST_UNSUPPORTEDE_NETWORK_CORS E_AUTH E_AUTH_FORBIDDEN E_ENV_UNSUPPORTED_CODECE_ENV_UNSUPPORTED_BROWSER E_ENV_PERMISSION_DENIED E_LOCALE_NOT_SUPPORTEDE_CONNECTION_LOST E_LOAD_TIMEOUT E_RECONNECT_FAILED

如果你在旧文档 / 旧代码里见到它们:它们从来没有真正触发过,对应的分支是死代码。

⚠️ E_CONNECTION_LOST 是个例外,值得单独说:它当初描述的就是「iframe 连接断了」—— 和后来的 E_FRAME_CRASHED 是同一件事。删它的理由不是「这件事不重要」,而是 当时没有任何检测能力,留在契约里是谎言(ADR-034 的原话:「一个我们暂时检测不到的错误码, 留在契约里是谎言;而删掉之后将来能检测了再加回来,是向后兼容的 minor,代价极低」)。

ADR-054 补上了检测能力,于是它以 E_FRAME_CRASHED 这个更准确的名字回来了 —— 这正是 ADR-034 那条原则的兑现,不是反悔。 换名是因为 "connection lost" 和网络错误 (E_NETWORK)语义上撞车,而实际发生的是 iframe 那一侧没了。 :::

警告码

W_* 不中断播放,走 compatwarning 事件,和 error 是两条流。

警告码含义有发射点
W_BROWSER_INCOMPATIBLEUC / 夸克等已知不兼容内核player-core/src/plugins/compat.ts
W_VERSION_MISMATCH两侧契约同 major、minor 不同frame-core/src/create-frame.ts(握手后)
W_EVENT_REJECTED收到一条过不了契约校验的入站事件,已丢弃frame-core/src/create-frame.ts(事件入口)· embed-helper/src/create-embed-listener.ts

W_EVENT_REJECTED(ADR-087)携带 rejectedEvent —— 被丢那条事件的名字, 类型是 string 而不是 EventName:它装的恰恰是本地契约不认识的名字。

⚠️ 它最常见的成因不是「iframe 坏了」,是「iframe 比 host 新」。 ADR-070 允许 同 major 不同 minor 的两端通信,而「新增事件是向后兼容的 minor」这条在严格校验下 不再自动成立:4.3.0 的 iframe 新增的事件,4.1.0 的 host 不认识,于是被丢。 收到它先看两端版本差多少,再怀疑 iframe 有 bug。

⚠️ 同一个事件名每个播放器实例只报一次 —— 被拒的往往是 timeupdate 那种 4 次/秒的高频事件,不去重的话告警本身就是噪音。

⚠️ inline 两面永不产生它 —— 那两条路没有 wire,事件从 player-core 直接进消费方, 不存在「不认识的东西」。这是结构性差异,不是能力缺失。

W_VERSION_MISMATCH 的 payload 比另一个多两个可选字段 (hostVersion / iframeVersion)—— 可选是为了不动 W_BROWSER_INCOMPATIBLE 那一侧, 详见 ADR-070。ua 对它是无关信息,留必填是因为改可选对 TS 消费方是 breaking。

两个码的分工:

判定后果
E_HANDSHAKE_VERSION_MISMATCH(错误)major 不同 → 不兼容降级 UI + 句柄进 dead 态
W_VERSION_MISMATCH(警告)同 major、minor 不同 → 兼容什么都不变,只报事实

⚠️ 它曾经是 ADR-034 漏掉的那一个,零发射点活了一个多月:那次清扫立的规则是 「错误码必须有发射点」,而它扫的是 ErrorCodeSchema,警告码住在 WarningCodeSchema 里, 整条没进扫描范围。现在两个 enum 都由 pnpm check:error-code-docs 双向钉着(#441)。

PlayerError 结构

ts
interface PlayerError {
  code: ErrorCode          // E_* 枚举(非自由字符串)
  message: string
  retryable: boolean       // 从 ERROR_META 推导,不由调用方传
  category: 'manifest' | 'media' | 'network' | 'auth' | 'env' | 'autoplay'
  cause?: ErrorCause       // 归一后的原始错误快照(debug 用)
}

interface ErrorCause {
  name: string             // 原始错误的 name;取不到时是 'Unknown'
  message: string          // 截断到 500 字符
  code?: number            // MediaError.code(1–4)等结构化码
  status?: number          // HTTP 状态码,区分 401 / 403
  // 其余**原始值**字段(string / number / boolean)原样保留;嵌套对象一律不收
}

cause 为什么是固定结构而不是 unknown(#270)

错误对象要跨 iframe,而 postMessage(Penpal 内部也是它)用的是结构化克隆makePlayerError 原先把原始错误对象直接塞进 cause,而 xgplayer 的错误上挂着 DOM MediaError —— 结构化克隆遇到它直接抛 DataCloneError,整条 envelope 发不出去。

后果不是「cause 丢了」,是消费方收到一条 code / category / retryable 全错的 E_INTERNAL (embed-app 的全局未捕获异常上报接住了那个 DataCloneError,把它包成了错误事件)。 inline 不过序列化,同一个失败给的是正确的 E_MEDIA_* —— cross-mode-parity 被打破

⚠️ 这里此前写的是「跨 iframe 传输,所以必须可 JSON 序列化」。规矩对,前提错: JSON.stringify 遇到不可序列化的东西静默丢弃,structuredClone 抛异常, 两者对同一件事的处理正好相反。契约按 JSON 语义写、传输按克隆语义跑,于是没人执行。

现在 causemakePlayerError 统一过 serializeCause(),字段全是原始类型, 结构化克隆不可能再抛apps/embed-app 的发送出口另有一道 wireSafe 兜底 —— 它防的不是这个 bug(已根治),是下一个不可克隆字段。

cause 仍然只用于 debug,业务判断请用 code / category / retryable

命令 rejection 的 code 跨 iframe 保真(#356)

命令失败(load() 跨内核、播放器未就绪……)不走 error 事件,走的是 RPC 的 rejection,和上面那条 envelope 是两条独立的路。它曾经有一条同类的漂移:

消费面2026-08-26 之前现在
react / vue inlineE_METHOD_NOT_SUPPORTEDE_METHOD_NOT_SUPPORTED
react-frame / vue-frameE_INTERNAL + category: 'env'E_METHOD_NOT_SUPPORTED

⚠️ 真因不是「penpal 过不去」,是载具选错了。 penpal 6.2.2 序列化 rejection 时 只对 returnValue instanceof Error 的值剥属性(只留 name / message / stack), 不是 Error 的值原样走结构化克隆,接收侧也不做还原。所以 embed-app 改抛普通对象 (command-rejection.ts),playerError 就跟着过来了。

class ... extends Error 不行 —— penpal 认的是 instanceof,继承一样命中。

宿主侧(frame-core/src/to-player-error.ts)对解包出来的 playerError 必须过 PlayerErrorSchema:它现在来自跨源 iframe,不再是本进程构造的,裸信任等于让对端 往消费方的错误分支里塞任意 code。校验不过就退回 E_INTERNAL

四条消费面同一个期望,由 e2e/specs/integration/source-switch.spec.ts 在浏览器里钉住 (静态 iframe 那条路结构性没有命令回程,不参与这条)。

详细说明

权威来源:packages/protocol/src/errors.ts。另见 USER-GUIDE.md § 29 完整错误码表 + 用户建议。