@video-lab/protocol 1.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -301,6 +301,17 @@ const DanmakuConfigSchema = z.object({
301
301
  * 定义在这里而不是 presets.ts,是为了让 PlayerConfig 能引用它而不产生循环依赖。
302
302
  */
303
303
  const PresetNameSchema = z.enum(["homepage-preview"]);
304
+ /** 内置控制栏的初始化显示策略。false 隐藏入口;true/省略仍遵守平台能力。 */
305
+ const ControlVisibilityConfigSchema = z.object({
306
+ play: z.boolean().optional(),
307
+ progress: z.boolean().optional(),
308
+ time: z.boolean().optional(),
309
+ volume: z.boolean().optional(),
310
+ playbackRate: z.boolean().optional(),
311
+ fullscreen: z.boolean().optional(),
312
+ cssFullscreen: z.boolean().optional(),
313
+ pip: z.boolean().optional()
314
+ }).strict();
304
315
  /**
305
316
  * 播放器初始化配置。iframe 模式下握手成功后作为第一条 command 下发,
306
317
  * 所以必须整体可 JSON 序列化。
@@ -356,6 +367,12 @@ const PlayerConfigSchema = z.object({
356
367
  * `boolean | { minimal?: boolean }` 是 **minor 不是 breaking**,不是单向门。
357
368
  */
358
369
  controls: z.boolean().optional(),
370
+ /** 初始化时是否显示默认错误覆盖层。默认开启;关闭不影响错误事件和恢复。 */
371
+ showErrorOverlay: z.boolean().optional(),
372
+ /** 运行时 Loading 展示归属;false 仅隐藏视觉/ARIA,恢复仍由 SDK 执行。 */
373
+ showLoadingOverlay: z.boolean().optional(),
374
+ /** 独立隐藏内置控件;只在创建时生效,修改后由宿主显式重建。 */
375
+ controlVisibility: ControlVisibilityConfigSchema.optional(),
359
376
  /** 是否响应用户交互。首页预览卡片用 false,点击透传给外层卡片 */
360
377
  interactive: z.boolean().optional(),
361
378
  poster: PosterConfigSchema.optional(),
@@ -783,12 +800,13 @@ function normalizeUserAction(raw) {
783
800
  const r = raw;
784
801
  const action = r.action;
785
802
  if (typeof action !== "string" || !ALLOWED.has(action)) return null;
786
- const first = (Array.isArray(r.props) ? r.props : [])[0] ?? null;
803
+ const first = (Array.isArray(r.props) ? r.props : [])[0];
804
+ const change = typeof first === "string" ? r : typeof first === "object" && first !== null ? first : null;
787
805
  return {
788
806
  action,
789
807
  source: typeof r.pluginName === "string" ? r.pluginName : "player",
790
- from: primitive(first?.from),
791
- to: primitive(first?.to)
808
+ from: primitive(change?.from),
809
+ to: primitive(change?.to)
792
810
  };
793
811
  }
794
812
  /**
@@ -813,6 +831,57 @@ function normalizeFrameFreeze(raw) {
813
831
  };
814
832
  }
815
833
  //#endregion
834
+ //#region src/page-fullscreen.ts
835
+ /** 专用网页全屏传输,不接收普通播放器命令;版本独立协商,旧端不会假定支持。 */
836
+ const base$1 = {
837
+ channel: z.literal("video-lab:page-fullscreen"),
838
+ version: z.literal(1),
839
+ session: z.string().min(1)
840
+ };
841
+ /** 静态 iframe 的受限双向消息,接收方还必须校验 origin 与 source。 */
842
+ const PageFullscreenMessageSchema = z.discriminatedUnion("type", [
843
+ z.object({
844
+ ...base$1,
845
+ type: z.literal("hello")
846
+ }),
847
+ z.object({
848
+ ...base$1,
849
+ type: z.literal("ready"),
850
+ instance: z.string().min(1)
851
+ }),
852
+ z.object({
853
+ ...base$1,
854
+ type: z.literal("request"),
855
+ instance: z.string().min(1),
856
+ id: z.number().int().positive(),
857
+ active: z.boolean()
858
+ }),
859
+ z.object({
860
+ ...base$1,
861
+ type: z.literal("state"),
862
+ instance: z.string().min(1),
863
+ revision: z.number().int().nonnegative(),
864
+ active: z.boolean()
865
+ }),
866
+ z.object({
867
+ ...base$1,
868
+ type: z.literal("ack"),
869
+ instance: z.string().min(1),
870
+ revision: z.number().int().nonnegative()
871
+ }),
872
+ z.object({
873
+ ...base$1,
874
+ type: z.literal("disconnect")
875
+ })
876
+ ]);
877
+ /** Penpal 控制面确认;仅在独立网页全屏能力协商后调用。 */
878
+ const PageFullscreenControlSchema = z.object({
879
+ available: z.boolean(),
880
+ active: z.boolean()
881
+ });
882
+ /** 宿主布局确认后的实际网页全屏状态。 */
883
+ const PageFullscreenStateSchema = z.object({ active: z.boolean() });
884
+ //#endregion
816
885
  //#region src/playback-context.ts
817
886
  /**
818
887
  * 播放内核 —— **实际选中的那个**,不是消费方声明的。
@@ -980,124 +1049,133 @@ function redactSourceUrl(url) {
980
1049
  };
981
1050
  }
982
1051
  }
983
- //#endregion
984
- //#region src/events.ts
985
- /**
986
- * 清晰度档位。`level` 是索引,传给 `setQuality` 命令用。
987
- */
988
- const QualityLevelSchema = z.object({
989
- level: z.number(),
990
- label: z.string().optional(),
991
- height: z.number().optional(),
992
- bitrate: z.number().optional()
993
- });
994
- /**
995
- * 可用字幕轨(player 加载后暴露给消费方)。`id` 是索引,传给 `setSubtitle` 命令用。
996
- *
997
- * 和输入侧的 {@link SubtitleTrack}(url/content 两种 `mode`)分开:那个是**消费方喂进来**的
998
- * 原始字幕描述,这个是 player **加载后回报**的、可切换的轨道清单(同 QualityLevel 之于 quality)。
999
- */
1000
- const SubtitleTrackInfoSchema = z.object({
1001
- /** 索引(= source.subtitles 数组下标),`setSubtitle({ id })` 用它引用轨道 */
1002
- id: z.number(),
1003
- /** BCP-47,如 `'en'` / `'zh-CN'` / `'th'` */
1004
- locale: z.string(),
1005
- /** 展示名,如 `'English'` / `'中文'` */
1006
- label: z.string()
1007
- });
1008
- /**
1009
- * 「现在能不能正常出画面」的原因枚举({@link PlayerEventSchema} 的 `playablechange`)。
1010
- *
1011
- * 优先级(高的压低的,同时命中时报最严重的那个):
1012
- * `error` > `frame_disconnected` > `autoplay_blocked` > `reconnecting` > `stalled`
1013
- * > `buffering` > `initializing` > `degraded` > `ok`
1014
- *
1015
- * **命名**:枚举值用 `snake_case`,与事件名(全小写连写)是两套命名空间 ——
1016
- * 事件名对齐 HTML5 媒体事件,枚举值是 payload 里的数据,`autoplay_blocked`
1017
- * 比 `autoplayblocked` 好读。既有的 `phase: 'start' | 'end'` 是单词故看不出区别。
1018
- */
1019
- const PlayableReasonSchema = z.enum([
1020
- "ok",
1021
- "initializing",
1022
- "buffering",
1023
- "stalled",
1024
- "reconnecting",
1025
- "autoplay_blocked",
1026
- "error",
1027
- "frame_disconnected",
1028
- "degraded"
1029
- ]);
1030
1052
  z.enum([
1031
1053
  "detected",
1054
+ "backoff",
1032
1055
  "attempting",
1033
1056
  "validating",
1034
1057
  "recovered",
1035
1058
  "failed",
1036
1059
  "cancelled"
1037
1060
  ]);
1038
- /** SDK 实际执行的恢复策略(ADR-079)。 */
1061
+ /** 策略变化共享 episode,不创建新的预算。 */
1039
1062
  const RecoveryStrategySchema = z.enum([
1040
1063
  "reconnect",
1041
1064
  "media_recovery",
1042
1065
  "visibility_reload"
1043
1066
  ]);
1044
- /** 恢复由错误、用户操作还是可见性变化触发。 */
1067
+ /** 触发原因与实际执行策略分别记录。 */
1045
1068
  const RecoveryTriggerSchema = z.enum([
1046
1069
  "error",
1047
1070
  "manual",
1048
- "visibility"
1071
+ "visibility",
1072
+ "stall",
1073
+ "startup_timeout"
1049
1074
  ]);
1050
- const RecoveryPayloadBase = {
1051
- /** 同一播放器实例内递增的恢复关联号。 */
1075
+ const base = {
1076
+ sessionId: z.string().min(1),
1052
1077
  recoveryId: z.number().int().positive(),
1053
1078
  strategy: RecoveryStrategySchema,
1054
1079
  trigger: RecoveryTriggerSchema,
1055
- /** 当前实际恢复轮次,从 1 开始。 */
1056
- attempt: z.number().int().positive(),
1057
- /** 该策略允许的最大恢复轮次。 */
1080
+ attempt: z.number().int().nonnegative(),
1058
1081
  maxAttempts: z.number().int().positive(),
1059
- /** 有错误起因时复用既有契约错误码;手动恢复可省略。 */
1082
+ /** 从 detected 起,源端用单调时钟测量的毫秒数。 */
1083
+ elapsedMs: z.number().finite().nonnegative(),
1060
1084
  reason: ErrorCodeSchema.optional()
1061
1085
  };
1062
- /**
1063
- * 可验证恢复的单条生命周期记录(ADR-079)。
1064
- *
1065
- * 终态字段按 `phase` 严格区分:`recovered` 只能声明位置推进这一种验证证据;
1066
- * `failed` / `cancelled` 只能携带各自登记的结束原因。`strict()` 刻意拒绝把终态字段
1067
- * 混入检测或验证阶段,避免上报端把“正在验证”误读成“已经恢复”。
1068
- */
1086
+ const issued = {
1087
+ ...base,
1088
+ attempt: z.number().int().positive()
1089
+ };
1090
+ /** 统一恢复的单条事实;取消不伪报成功,跨会话归属不依赖相邻事件猜测。 */
1069
1091
  const RecoveryPayloadSchema = z.discriminatedUnion("phase", [
1070
1092
  z.object({
1071
- ...RecoveryPayloadBase,
1093
+ ...base,
1072
1094
  phase: z.literal("detected")
1073
1095
  }).strict(),
1074
1096
  z.object({
1075
- ...RecoveryPayloadBase,
1097
+ ...base,
1098
+ phase: z.literal("backoff")
1099
+ }).strict(),
1100
+ z.object({
1101
+ ...issued,
1076
1102
  phase: z.literal("attempting")
1077
1103
  }).strict(),
1078
1104
  z.object({
1079
- ...RecoveryPayloadBase,
1105
+ ...issued,
1080
1106
  phase: z.literal("validating")
1081
1107
  }).strict(),
1082
1108
  z.object({
1083
- ...RecoveryPayloadBase,
1109
+ ...issued,
1084
1110
  phase: z.literal("recovered"),
1085
1111
  validatedBy: z.literal("playing_position_advance")
1086
1112
  }).strict(),
1087
1113
  z.object({
1088
- ...RecoveryPayloadBase,
1114
+ ...base,
1089
1115
  phase: z.literal("failed"),
1090
1116
  outcome: z.enum(["timeout", "attempts_exhausted"])
1091
1117
  }).strict(),
1092
1118
  z.object({
1093
- ...RecoveryPayloadBase,
1119
+ ...base,
1094
1120
  phase: z.literal("cancelled"),
1095
1121
  outcome: z.enum([
1096
1122
  "source_changed",
1097
1123
  "destroyed",
1098
- "superseded"
1124
+ "superseded",
1125
+ "user_paused",
1126
+ "natural_recovery"
1099
1127
  ])
1100
1128
  }).strict()
1129
+ ]).refine((value) => value.attempt <= value.maxAttempts, {
1130
+ message: "attempt exceeds episode budget",
1131
+ path: ["attempt"]
1132
+ });
1133
+ //#endregion
1134
+ //#region src/events.ts
1135
+ /**
1136
+ * 清晰度档位。`level` 是索引,传给 `setQuality` 命令用。
1137
+ */
1138
+ const QualityLevelSchema = z.object({
1139
+ level: z.number(),
1140
+ label: z.string().optional(),
1141
+ height: z.number().optional(),
1142
+ bitrate: z.number().optional()
1143
+ });
1144
+ /**
1145
+ * 可用字幕轨(player 加载后暴露给消费方)。`id` 是索引,传给 `setSubtitle` 命令用。
1146
+ *
1147
+ * 和输入侧的 {@link SubtitleTrack}(url/content 两种 `mode`)分开:那个是**消费方喂进来**的
1148
+ * 原始字幕描述,这个是 player **加载后回报**的、可切换的轨道清单(同 QualityLevel 之于 quality)。
1149
+ */
1150
+ const SubtitleTrackInfoSchema = z.object({
1151
+ /** 索引(= source.subtitles 数组下标),`setSubtitle({ id })` 用它引用轨道 */
1152
+ id: z.number(),
1153
+ /** BCP-47,如 `'en'` / `'zh-CN'` / `'th'` */
1154
+ locale: z.string(),
1155
+ /** 展示名,如 `'English'` / `'中文'` */
1156
+ label: z.string()
1157
+ });
1158
+ /**
1159
+ * 「现在能不能正常出画面」的原因枚举({@link PlayerEventSchema} 的 `playablechange`)。
1160
+ *
1161
+ * 优先级(高的压低的,同时命中时报最严重的那个):
1162
+ * `error` > `frame_disconnected` > `autoplay_blocked` > `reconnecting` > `stalled`
1163
+ * > `buffering` > `initializing` > `degraded` > `ok`
1164
+ *
1165
+ * **命名**:枚举值用 `snake_case`,与事件名(全小写连写)是两套命名空间 ——
1166
+ * 事件名对齐 HTML5 媒体事件,枚举值是 payload 里的数据,`autoplay_blocked`
1167
+ * 比 `autoplayblocked` 好读。既有的 `phase: 'start' | 'end'` 是单词故看不出区别。
1168
+ */
1169
+ const PlayableReasonSchema = z.enum([
1170
+ "ok",
1171
+ "initializing",
1172
+ "buffering",
1173
+ "stalled",
1174
+ "reconnecting",
1175
+ "autoplay_blocked",
1176
+ "error",
1177
+ "frame_disconnected",
1178
+ "degraded"
1101
1179
  ]);
1102
1180
  /**
1103
1181
  * 事件。player → host(iframe 模式),或 player-core → 消费方(inline 模式)。
@@ -1109,6 +1187,10 @@ const RecoveryPayloadSchema = z.discriminatedUnion("phase", [
1109
1187
  * 权威来源:docs/protocol/events.md + ARCHITECTURE.md § 8.4。
1110
1188
  */
1111
1189
  const PlayerEventSchema = z.discriminatedUnion("event", [
1190
+ z.object({
1191
+ event: z.literal("pagefullscreenchange"),
1192
+ payload: PageFullscreenStateSchema
1193
+ }),
1112
1194
  z.object({
1113
1195
  event: z.literal("sourceroute"),
1114
1196
  payload: SourceRoutePayloadSchema
@@ -1199,43 +1281,6 @@ id: z.number().nullable() })
1199
1281
  event: z.literal("autoplayblocked"),
1200
1282
  payload: z.object({})
1201
1283
  }),
1202
- z.object({
1203
- event: z.literal("reconnectstart"),
1204
- payload: z.object({
1205
- attempt: z.number(),
1206
- maxAttempts: z.number(),
1207
- /**
1208
- * 触发这一轮重连的**契约错误码**;手动 `reconnect()` 时缺省(没有触发它的错误)。
1209
- *
1210
- * ⚠️ **这里曾经是 `z.string()`,而那让它在服务端聚合不了**(ADR-069)——
1211
- * 消费方拿到的类型是 `string`,`switch` 不了、也没有任何东西挡住将来漂成别的写法。
1212
- * **而源头从来就是一个契约错误码**:`plugins/reconnect.ts` 传的是
1213
- * `mapXgplayerError(err).code`,没有第二个发射点。
1214
- *
1215
- * **复用 `ErrorCodeSchema`,不新起一套「重连原因」枚举** —— 理由同 ADR-062 ③:
1216
- * 两套表会让同一条内核错误在 `error` 和 `reconnectstart` 两条通道上给出**互相矛盾**的分类,
1217
- * 而那种漂移没人查得出来。
1218
- *
1219
- * ⚠️ **不要照着「今天实际只会出现哪几个码」去收窄。** 那要复刻
1220
- * `ReconnectPlugin` 的两道过滤(`retryable === true` 且 `category !== 'media'`),
1221
- * 而那两道闸是**实现细节**,改一行就和契约对不上了 —— 那正是第二份会漂的副本。
1222
- */
1223
- reason: ErrorCodeSchema.optional(),
1224
- nextDelayMs: z.number().optional()
1225
- })
1226
- }),
1227
- z.object({
1228
- event: z.literal("reconnectsuccess"),
1229
- payload: z.object({ attempts: z.number() })
1230
- }),
1231
- z.object({
1232
- event: z.literal("reconnectfailed"),
1233
- payload: z.object({
1234
- attempts: z.number(),
1235
- /** 最后一轮的触发错误码。语义与取值同 `reconnectstart.reason`(ADR-069) */
1236
- reason: ErrorCodeSchema.optional()
1237
- })
1238
- }),
1239
1284
  z.object({
1240
1285
  event: z.literal("recovery"),
1241
1286
  payload: RecoveryPayloadSchema
@@ -1312,7 +1357,15 @@ id: z.number().nullable() })
1312
1357
  *(正常启动,该等),握手失败降级后为 `false`(连不上了,该露重试入口)。
1313
1358
  * 两个阶段共用一个 reason,靠这个字段区分 —— 这正是它存在的意义。
1314
1359
  */
1315
- recoverable: z.boolean()
1360
+ recoverable: z.boolean(),
1361
+ /** 下一步动作能力;宿主无须按错误类型自行推断。 */
1362
+ action: z.enum([
1363
+ "retry",
1364
+ "play",
1365
+ "replace-source",
1366
+ "recreate-frame",
1367
+ "none"
1368
+ ]).optional()
1316
1369
  })
1317
1370
  }),
1318
1371
  z.object({
@@ -1456,6 +1509,28 @@ function toErrorEvent(err) {
1456
1509
  };
1457
1510
  }
1458
1511
  //#endregion
1512
+ //#region src/delivery.ts
1513
+ /**
1514
+ * 一次已送达播放器事件的生产端证据。
1515
+ *
1516
+ * `sequence` 只在同一个 `producerSessionId` 内严格递增;`deliveryId` 也只承诺在
1517
+ * 该会话内唯一。`occurredAtMs` 是生产端的墙钟,不是宿主接收时间,不能跨设备相减。
1518
+ * 消费者可用 `(producerSessionId, sequence)` 排序、用
1519
+ * `(producerSessionId, deliveryId)` 去重,并拒绝旧会话的迟到事件。
1520
+ */
1521
+ const DeliveredPlayerEventSchema = z.object({
1522
+ /** 未变形的公开业务事件;旧 raw 出口继续单独交付它。 */
1523
+ event: PlayerEventSchema,
1524
+ /** 每次 player/frame producer 生命周期唯一的会话标识。 */
1525
+ producerSessionId: z.string().min(1),
1526
+ /** 在该 producer session 内唯一且重传时保持稳定的投递标识。 */
1527
+ deliveryId: z.string().min(1),
1528
+ /** 生产端创建该事件时读取的 Unix ms 墙钟。 */
1529
+ occurredAtMs: z.number().finite(),
1530
+ /** 在该 producer session 内从 1 开始严格递增的序号。 */
1531
+ sequence: z.number().int().positive()
1532
+ });
1533
+ //#endregion
1459
1534
  //#region src/methods.ts
1460
1535
  /**
1461
1536
  * 命令。host → iframe(iframe 模式),或消费方 → player-core(inline 模式)。
@@ -1526,10 +1601,8 @@ const CommandSchema = z.discriminatedUnion("method", [
1526
1601
  params: z.object({ source: MediaSourceSchema })
1527
1602
  }),
1528
1603
  z.object({
1529
- method: z.literal("reconnect"),
1530
- params: z.object({
1531
- /** true = 把重连计数清零,重新获得完整的 maxRetries 次机会 */
1532
- resetCounter: z.boolean().optional() }).optional()
1604
+ method: z.literal("retry"),
1605
+ params: z.object({}).strict().optional()
1533
1606
  }),
1534
1607
  z.object({
1535
1608
  method: z.literal("pushDanmaku"),
@@ -1561,36 +1634,11 @@ resetCounter: z.boolean().optional() }).optional()
1561
1634
  //#endregion
1562
1635
  //#region src/version.ts
1563
1636
  /**
1564
- * 契约版本。
1565
- *
1566
- * 注意:这和 iframe URL 里的 `version`(如 `'v1'`,见 ADR-023)不是一回事——
1567
- * 那个是 CDN 路径版本,这个是 host ↔ iframe 的通信契约版本。
1568
- *
1569
- * 冻结策略见 `packages/protocol/CLAUDE.md`:审查清单全过之后才升 1.0.0。
1570
- *
1571
- * **首发即 1.1.0**(2026-07-19):契约从未对外发布,故把首发前迭代出的全部能力面
1572
- * **折叠进首发契约**——与 8 个包的 npm 版本对齐,避免"包版本 1.1.0 却携带 1.0.0
1573
- * 契约常量"的双轴割裂(`tests/index.contract.test.ts` 有断言锁死这一致性)。
1574
- * 首发契约面**已包含**:
1575
- * - `stalled` 事件(HealthMonitor 卡顿测量,设计见 ADR-026)
1576
- * - 字幕控制侧:`setSubtitle` 命令 + `subtitlechange` 事件 + `ready` 的**可选** `subtitles` 字段(ADR-027)
1577
- * - 弹幕 streaming:`pushDanmaku` / `setDanmakuEnabled` / `clearDanmaku` 命令 + `PlayerConfig.danmaku`(可选)(ADR-028)
1578
- * - 场景预设收敛为唯一 `homepage-preview`(ADR-032)
1579
- *
1580
- * 上面几个 ADR 记录的是这些能力的**设计出处**,不是"冻结后独立发布的 minor"——它们在首发前
1581
- * 就已落地,故都是首发契约的一部分。**首发之后**再新增 method/event/字段才走 minor
1582
- * (1.x 向后兼容),破坏性变更须走 ADR + major。
1583
- *
1584
- * 为什么是 1.1.0 而不是 1.0.0:原计划锁 1.0.0(ADR-025),但首发前两项变更
1585
- * (全屏命令补完、preset 收敛)各带一条 minor changeset,changesets 的 `fixed` 组
1586
- * 把 8 个包统一推到 1.1.0。契约面确有变化(删了 4 个 preset),升 minor 名实相符;
1587
- * 且 {@link isContractCompatible} 在 `>=1.0.0` 时只比 major,1.0.0 ↔ 1.1.0 握手仍兼容。
1588
- *
1589
- * 值**从 package.json 派生**,不要改回硬编码 —— 契约常量必须与 npm 包版本
1590
- * 严格相等,而 changesets 只改 package.json。理由与实测数据见
1591
- * `docs/adr/ADR-036-contract-version-derived.md`。
1637
+ * host ↔ iframe 通信契约版本,独立于 npm fixed group 和 iframe 应用版本。
1638
+ * 唯一来源是 contract-version.json(ADR-094);只在真实协议变化时升级。
1639
+ * npm 包版本同步不能改变握手与 envelope 的版本。
1592
1640
  */
1593
- const CONTRACT_VERSION = "1.0.1";
1641
+ const CONTRACT_VERSION = "2.0.0";
1594
1642
  const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
1595
1643
  /**
1596
1644
  * 解析 semver 字符串。只接受严格的 `major.minor.patch`,
@@ -1681,8 +1729,18 @@ function createEnvelopeSchema(type, payload) {
1681
1729
  }
1682
1730
  /** 命令包裹(host → iframe) */
1683
1731
  const CommandEnvelopeSchema = createEnvelopeSchema("command", CommandSchema);
1684
- /** 事件包裹(iframe → host) */
1685
- const EventEnvelopeSchema = createEnvelopeSchema("event", PlayerEventSchema);
1732
+ const LegacyEventEnvelopeSchema = createEnvelopeSchema("event", PlayerEventSchema).extend({
1733
+ /** Older iframe builds omit all delivery evidence. */
1734
+ producerSessionId: z.undefined().optional(),
1735
+ sequence: z.undefined().optional()
1736
+ });
1737
+ const DeliveredEventEnvelopeSchema = createEnvelopeSchema("event", PlayerEventSchema).extend({
1738
+ /** Producer-side delivery evidence is atomic: both fields are required together. */
1739
+ producerSessionId: z.string().min(1),
1740
+ sequence: z.number().int().positive()
1741
+ });
1742
+ /** 事件包裹(iframe → host):旧包省略证据,新包同时携带 session 和 sequence。 */
1743
+ const EventEnvelopeSchema = z.union([LegacyEventEnvelopeSchema, DeliveredEventEnvelopeSchema]);
1686
1744
  /**
1687
1745
  * 响应包裹(iframe → host)。命令的结果一律是 void——
1688
1746
  * 播放状态通过事件回来,不塞在 response 里。
@@ -1693,10 +1751,10 @@ const ErrorEnvelopeSchema = createEnvelopeSchema("error", PlayerErrorSchema);
1693
1751
  /**
1694
1752
  * 任意包裹。收到消息时先用它 parse,再按 `type` 分支。
1695
1753
  *
1696
- * 用 discriminatedUnion 而不是 union:错误信息能精确到具体分支,
1697
- * 而不是把四个分支的失败原因全列一遍。
1754
+ * event 分支自身要区分旧包和带 delivery evidence 的新包,因此这里用 union。
1755
+ * 每个分支的 `type` 仍是具体字面量,TypeScript 仍可按 `type` 窄化。
1698
1756
  */
1699
- const EnvelopeSchema = z.discriminatedUnion("type", [
1757
+ const EnvelopeSchema = z.union([
1700
1758
  CommandEnvelopeSchema,
1701
1759
  EventEnvelopeSchema,
1702
1760
  ResponseEnvelopeSchema,
@@ -1724,6 +1782,13 @@ function commandEnvelope(payload, meta) {
1724
1782
  }
1725
1783
  /** 构造事件包裹(iframe → host) */
1726
1784
  function eventEnvelope(payload, meta) {
1785
+ if (meta.producerSessionId !== void 0 && meta.sequence !== void 0) return {
1786
+ ...envelopeBase(meta),
1787
+ type: "event",
1788
+ payload,
1789
+ producerSessionId: meta.producerSessionId,
1790
+ sequence: meta.sequence
1791
+ };
1727
1792
  return {
1728
1793
  ...envelopeBase(meta),
1729
1794
  type: "event",
@@ -1844,6 +1909,6 @@ function resolvePreset(preset, config) {
1844
1909
  return merged;
1845
1910
  }
1846
1911
  //#endregion
1847
- export { CONTRACT_VERSION, CommandEnvelopeSchema, CommandSchema, DanmakuConfigSchema, DanmakuItemSchema, ERROR_META, EnvelopeSchema, EnvelopeTypeSchema, ErrorCategorySchema, ErrorCauseSchema, ErrorCodeSchema, ErrorEnvelopeSchema, EventEnvelopeSchema, FirstFramePayloadSchema, FrameFreezePayloadSchema, HlsConfigSchema, LocaleConfigSchema, MediaMetadataSchema, MediaSourceSchema, MediaTypeSchema, MultiSourceObjectSchema, PLAYER_DESTROYED_MESSAGE, PRESETS, PauseImageConfigSchema, PlayableReasonSchema, PlaybackContextSchema, PlaybackKernelSchema, PlaybackRuntimeSchema, PlayerConfigSchema, PlayerErrorSchema, PlayerEventSchema, PosterConfigSchema, PresetNameSchema, QualityLevelSchema, RecoveryPayloadSchema, ResponseEnvelopeSchema, SingleSourceObjectSchema, SourceEntrySchema, SourceEntryTypeSchema, SourceRoutePayloadSchema, StreamKindSchema, SubtitleTrackInfoSchema, SubtitleTrackSchema, USER_ACTION_ALLOWLIST, UserActionPayloadSchema, WarningCodeSchema, commandEnvelope, createEnvelopeSchema, errorEnvelope, eventEnvelope, isContractCompatible, makeEventRejectedWarning, makePlayerError, normalizeFrameFreeze, normalizeUserAction, parseVersion, redactSourceUrl, resolveLocaleMessages, resolvePreset, responseEnvelope, serializeCause, toErrorEvent };
1912
+ export { CONTRACT_VERSION, CommandEnvelopeSchema, CommandSchema, ControlVisibilityConfigSchema, DanmakuConfigSchema, DanmakuItemSchema, DeliveredPlayerEventSchema, ERROR_META, EnvelopeSchema, EnvelopeTypeSchema, ErrorCategorySchema, ErrorCauseSchema, ErrorCodeSchema, ErrorEnvelopeSchema, EventEnvelopeSchema, FirstFramePayloadSchema, FrameFreezePayloadSchema, HlsConfigSchema, LocaleConfigSchema, MediaMetadataSchema, MediaSourceSchema, MediaTypeSchema, MultiSourceObjectSchema, PLAYER_DESTROYED_MESSAGE, PRESETS, PageFullscreenControlSchema, PageFullscreenMessageSchema, PageFullscreenStateSchema, PauseImageConfigSchema, PlayableReasonSchema, PlaybackContextSchema, PlaybackKernelSchema, PlaybackRuntimeSchema, PlayerConfigSchema, PlayerErrorSchema, PlayerEventSchema, PosterConfigSchema, PresetNameSchema, QualityLevelSchema, RecoveryPayloadSchema, ResponseEnvelopeSchema, SingleSourceObjectSchema, SourceEntrySchema, SourceEntryTypeSchema, SourceRoutePayloadSchema, StreamKindSchema, SubtitleTrackInfoSchema, SubtitleTrackSchema, USER_ACTION_ALLOWLIST, UserActionPayloadSchema, WarningCodeSchema, commandEnvelope, createEnvelopeSchema, errorEnvelope, eventEnvelope, isContractCompatible, makeEventRejectedWarning, makePlayerError, normalizeFrameFreeze, normalizeUserAction, parseVersion, redactSourceUrl, resolveLocaleMessages, resolvePreset, responseEnvelope, serializeCause, toErrorEvent };
1848
1913
 
1849
1914
  //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@video-lab/protocol",
3
- "version": "1.0.1",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "description": "Video Lab Player 契约层:Zod schema 定义命令 / 事件 / 错误码 / 配置,所有包的唯一事实源",
6
6
  "type": "module",