@video-lab/protocol 2.0.0 → 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/README.md +9 -2
- package/dist/index.cjs +216 -121
- package/dist/index.d.cts +13388 -6353
- package/dist/index.d.mts +13388 -6353
- package/dist/index.mjs +212 -122
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ Video Lab Player 的通信契约层。它定义 host 与 iframe 的消息格式
|
|
|
10
10
|
|:---|:---|
|
|
11
11
|
| `version` | `CONTRACT_VERSION` + 版本兼容判定 |
|
|
12
12
|
| `envelope` | 通信包裹:`command` / `event` / `response` / `error` |
|
|
13
|
-
| `methods` |
|
|
14
|
-
| `events` |
|
|
13
|
+
| `methods` | 17 条命令(play / pause / seek / load / setSubtitle / pushDanmaku / …) |
|
|
14
|
+
| `events` | 27 个事件(sourceroute / ready / timeupdate / error / recovery / subtitlechange / stalled / …) |
|
|
15
|
+
| `delivery` | `DeliveredPlayerEvent`:带生产端 session、稳定 ID、发生时间与序号的完整事件投递证据 |
|
|
15
16
|
| `errors` | 29 个错误码 + 2 个警告码 + `PlayerError` |
|
|
16
17
|
| `configs` | `MediaSource`、`PlayerConfig`、poster / locale / danmaku / controls |
|
|
17
18
|
| `presets` | 场景预设(如 `homepage-preview`)与 `resolvePreset` |
|
|
@@ -51,6 +52,8 @@ const config = resolvePreset('homepage-preview', { source: 'a.m3u8', autoplay: f
|
|
|
51
52
|
|
|
52
53
|
**事件名是全小写连写**(`timeupdate` / `autoplayblocked`),对齐 HTML5 媒体事件。这是通信层名称;Vue 映射为 `time-update`,React 映射为 `onTimeUpdate`。
|
|
53
54
|
|
|
55
|
+
**完整事件流优先使用 `DeliveredPlayerEvent`**。它在兼容的新增 outlet 上提供 `producerSessionId`、`deliveryId`、`occurredAtMs`、`sequence`,适合跨重连排序与去重;旧 `PlayerEvent` 回调保持原样。旧版 iframe 可以省略该证据,宿主仍会收到 raw 事件。
|
|
56
|
+
|
|
54
57
|
**`retryable` 是给自动重连看的信号**,不是“用户能不能点重试按钮”。判断标准是同一请求原样重发是否可能成功。因此 `E_AUTH_EXPIRED` 为 `false`:SDK 不刷新签名,原样重试必然再次失败。
|
|
55
58
|
|
|
56
59
|
**`source.onBeforeRequest` 不在 wire 契约里**。函数不可序列化,过不了 iframe 边界;它是宿主侧的 hook,由 inline 模式的 player-core 直接消费。且它在 MP4 和 iOS Safari 播 HLS 时**不生效**。
|
|
@@ -78,3 +81,7 @@ pnpm --filter @video-lab/protocol test
|
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
契约测试覆盖消费包实际使用的事件、命令与错误码,防止通信契约与消费包行为漂移。
|
|
84
|
+
|
|
85
|
+
## 宿主网页全屏
|
|
86
|
+
|
|
87
|
+
导出 `PageFullscreenAdapter` / `PageFullscreenState` 类型、实际状态事件 `pagefullscreenchange`,以及专用通道的 `PageFullscreenMessageSchema` / `PageFullscreenControlSchema` / `PageFullscreenStateSchema`。宿主函数与 DOM 不进入 PlayerConfig。消费面调用和升级边界见[网页全屏接入](../../docs/guides/PAGE-FULLSCREEN.md)。
|
package/dist/index.cjs
CHANGED
|
@@ -302,6 +302,17 @@ const DanmakuConfigSchema = zod.z.object({
|
|
|
302
302
|
* 定义在这里而不是 presets.ts,是为了让 PlayerConfig 能引用它而不产生循环依赖。
|
|
303
303
|
*/
|
|
304
304
|
const PresetNameSchema = zod.z.enum(["homepage-preview"]);
|
|
305
|
+
/** 内置控制栏的初始化显示策略。false 隐藏入口;true/省略仍遵守平台能力。 */
|
|
306
|
+
const ControlVisibilityConfigSchema = zod.z.object({
|
|
307
|
+
play: zod.z.boolean().optional(),
|
|
308
|
+
progress: zod.z.boolean().optional(),
|
|
309
|
+
time: zod.z.boolean().optional(),
|
|
310
|
+
volume: zod.z.boolean().optional(),
|
|
311
|
+
playbackRate: zod.z.boolean().optional(),
|
|
312
|
+
fullscreen: zod.z.boolean().optional(),
|
|
313
|
+
cssFullscreen: zod.z.boolean().optional(),
|
|
314
|
+
pip: zod.z.boolean().optional()
|
|
315
|
+
}).strict();
|
|
305
316
|
/**
|
|
306
317
|
* 播放器初始化配置。iframe 模式下握手成功后作为第一条 command 下发,
|
|
307
318
|
* 所以必须整体可 JSON 序列化。
|
|
@@ -357,6 +368,12 @@ const PlayerConfigSchema = zod.z.object({
|
|
|
357
368
|
* `boolean | { minimal?: boolean }` 是 **minor 不是 breaking**,不是单向门。
|
|
358
369
|
*/
|
|
359
370
|
controls: zod.z.boolean().optional(),
|
|
371
|
+
/** 初始化时是否显示默认错误覆盖层。默认开启;关闭不影响错误事件和恢复。 */
|
|
372
|
+
showErrorOverlay: zod.z.boolean().optional(),
|
|
373
|
+
/** 运行时 Loading 展示归属;false 仅隐藏视觉/ARIA,恢复仍由 SDK 执行。 */
|
|
374
|
+
showLoadingOverlay: zod.z.boolean().optional(),
|
|
375
|
+
/** 独立隐藏内置控件;只在创建时生效,修改后由宿主显式重建。 */
|
|
376
|
+
controlVisibility: ControlVisibilityConfigSchema.optional(),
|
|
360
377
|
/** 是否响应用户交互。首页预览卡片用 false,点击透传给外层卡片 */
|
|
361
378
|
interactive: zod.z.boolean().optional(),
|
|
362
379
|
poster: PosterConfigSchema.optional(),
|
|
@@ -784,12 +801,13 @@ function normalizeUserAction(raw) {
|
|
|
784
801
|
const r = raw;
|
|
785
802
|
const action = r.action;
|
|
786
803
|
if (typeof action !== "string" || !ALLOWED.has(action)) return null;
|
|
787
|
-
const first = (Array.isArray(r.props) ? r.props : [])[0]
|
|
804
|
+
const first = (Array.isArray(r.props) ? r.props : [])[0];
|
|
805
|
+
const change = typeof first === "string" ? r : typeof first === "object" && first !== null ? first : null;
|
|
788
806
|
return {
|
|
789
807
|
action,
|
|
790
808
|
source: typeof r.pluginName === "string" ? r.pluginName : "player",
|
|
791
|
-
from: primitive(
|
|
792
|
-
to: primitive(
|
|
809
|
+
from: primitive(change?.from),
|
|
810
|
+
to: primitive(change?.to)
|
|
793
811
|
};
|
|
794
812
|
}
|
|
795
813
|
/**
|
|
@@ -814,6 +832,57 @@ function normalizeFrameFreeze(raw) {
|
|
|
814
832
|
};
|
|
815
833
|
}
|
|
816
834
|
//#endregion
|
|
835
|
+
//#region src/page-fullscreen.ts
|
|
836
|
+
/** 专用网页全屏传输,不接收普通播放器命令;版本独立协商,旧端不会假定支持。 */
|
|
837
|
+
const base$1 = {
|
|
838
|
+
channel: zod.z.literal("video-lab:page-fullscreen"),
|
|
839
|
+
version: zod.z.literal(1),
|
|
840
|
+
session: zod.z.string().min(1)
|
|
841
|
+
};
|
|
842
|
+
/** 静态 iframe 的受限双向消息,接收方还必须校验 origin 与 source。 */
|
|
843
|
+
const PageFullscreenMessageSchema = zod.z.discriminatedUnion("type", [
|
|
844
|
+
zod.z.object({
|
|
845
|
+
...base$1,
|
|
846
|
+
type: zod.z.literal("hello")
|
|
847
|
+
}),
|
|
848
|
+
zod.z.object({
|
|
849
|
+
...base$1,
|
|
850
|
+
type: zod.z.literal("ready"),
|
|
851
|
+
instance: zod.z.string().min(1)
|
|
852
|
+
}),
|
|
853
|
+
zod.z.object({
|
|
854
|
+
...base$1,
|
|
855
|
+
type: zod.z.literal("request"),
|
|
856
|
+
instance: zod.z.string().min(1),
|
|
857
|
+
id: zod.z.number().int().positive(),
|
|
858
|
+
active: zod.z.boolean()
|
|
859
|
+
}),
|
|
860
|
+
zod.z.object({
|
|
861
|
+
...base$1,
|
|
862
|
+
type: zod.z.literal("state"),
|
|
863
|
+
instance: zod.z.string().min(1),
|
|
864
|
+
revision: zod.z.number().int().nonnegative(),
|
|
865
|
+
active: zod.z.boolean()
|
|
866
|
+
}),
|
|
867
|
+
zod.z.object({
|
|
868
|
+
...base$1,
|
|
869
|
+
type: zod.z.literal("ack"),
|
|
870
|
+
instance: zod.z.string().min(1),
|
|
871
|
+
revision: zod.z.number().int().nonnegative()
|
|
872
|
+
}),
|
|
873
|
+
zod.z.object({
|
|
874
|
+
...base$1,
|
|
875
|
+
type: zod.z.literal("disconnect")
|
|
876
|
+
})
|
|
877
|
+
]);
|
|
878
|
+
/** Penpal 控制面确认;仅在独立网页全屏能力协商后调用。 */
|
|
879
|
+
const PageFullscreenControlSchema = zod.z.object({
|
|
880
|
+
available: zod.z.boolean(),
|
|
881
|
+
active: zod.z.boolean()
|
|
882
|
+
});
|
|
883
|
+
/** 宿主布局确认后的实际网页全屏状态。 */
|
|
884
|
+
const PageFullscreenStateSchema = zod.z.object({ active: zod.z.boolean() });
|
|
885
|
+
//#endregion
|
|
817
886
|
//#region src/playback-context.ts
|
|
818
887
|
/**
|
|
819
888
|
* 播放内核 —— **实际选中的那个**,不是消费方声明的。
|
|
@@ -981,124 +1050,133 @@ function redactSourceUrl(url) {
|
|
|
981
1050
|
};
|
|
982
1051
|
}
|
|
983
1052
|
}
|
|
984
|
-
//#endregion
|
|
985
|
-
//#region src/events.ts
|
|
986
|
-
/**
|
|
987
|
-
* 清晰度档位。`level` 是索引,传给 `setQuality` 命令用。
|
|
988
|
-
*/
|
|
989
|
-
const QualityLevelSchema = zod.z.object({
|
|
990
|
-
level: zod.z.number(),
|
|
991
|
-
label: zod.z.string().optional(),
|
|
992
|
-
height: zod.z.number().optional(),
|
|
993
|
-
bitrate: zod.z.number().optional()
|
|
994
|
-
});
|
|
995
|
-
/**
|
|
996
|
-
* 可用字幕轨(player 加载后暴露给消费方)。`id` 是索引,传给 `setSubtitle` 命令用。
|
|
997
|
-
*
|
|
998
|
-
* 和输入侧的 {@link SubtitleTrack}(url/content 两种 `mode`)分开:那个是**消费方喂进来**的
|
|
999
|
-
* 原始字幕描述,这个是 player **加载后回报**的、可切换的轨道清单(同 QualityLevel 之于 quality)。
|
|
1000
|
-
*/
|
|
1001
|
-
const SubtitleTrackInfoSchema = zod.z.object({
|
|
1002
|
-
/** 索引(= source.subtitles 数组下标),`setSubtitle({ id })` 用它引用轨道 */
|
|
1003
|
-
id: zod.z.number(),
|
|
1004
|
-
/** BCP-47,如 `'en'` / `'zh-CN'` / `'th'` */
|
|
1005
|
-
locale: zod.z.string(),
|
|
1006
|
-
/** 展示名,如 `'English'` / `'中文'` */
|
|
1007
|
-
label: zod.z.string()
|
|
1008
|
-
});
|
|
1009
|
-
/**
|
|
1010
|
-
* 「现在能不能正常出画面」的原因枚举({@link PlayerEventSchema} 的 `playablechange`)。
|
|
1011
|
-
*
|
|
1012
|
-
* 优先级(高的压低的,同时命中时报最严重的那个):
|
|
1013
|
-
* `error` > `frame_disconnected` > `autoplay_blocked` > `reconnecting` > `stalled`
|
|
1014
|
-
* > `buffering` > `initializing` > `degraded` > `ok`
|
|
1015
|
-
*
|
|
1016
|
-
* **命名**:枚举值用 `snake_case`,与事件名(全小写连写)是两套命名空间 ——
|
|
1017
|
-
* 事件名对齐 HTML5 媒体事件,枚举值是 payload 里的数据,`autoplay_blocked`
|
|
1018
|
-
* 比 `autoplayblocked` 好读。既有的 `phase: 'start' | 'end'` 是单词故看不出区别。
|
|
1019
|
-
*/
|
|
1020
|
-
const PlayableReasonSchema = zod.z.enum([
|
|
1021
|
-
"ok",
|
|
1022
|
-
"initializing",
|
|
1023
|
-
"buffering",
|
|
1024
|
-
"stalled",
|
|
1025
|
-
"reconnecting",
|
|
1026
|
-
"autoplay_blocked",
|
|
1027
|
-
"error",
|
|
1028
|
-
"frame_disconnected",
|
|
1029
|
-
"degraded"
|
|
1030
|
-
]);
|
|
1031
1053
|
zod.z.enum([
|
|
1032
1054
|
"detected",
|
|
1055
|
+
"backoff",
|
|
1033
1056
|
"attempting",
|
|
1034
1057
|
"validating",
|
|
1035
1058
|
"recovered",
|
|
1036
1059
|
"failed",
|
|
1037
1060
|
"cancelled"
|
|
1038
1061
|
]);
|
|
1039
|
-
/**
|
|
1062
|
+
/** 策略变化共享 episode,不创建新的预算。 */
|
|
1040
1063
|
const RecoveryStrategySchema = zod.z.enum([
|
|
1041
1064
|
"reconnect",
|
|
1042
1065
|
"media_recovery",
|
|
1043
1066
|
"visibility_reload"
|
|
1044
1067
|
]);
|
|
1045
|
-
/**
|
|
1068
|
+
/** 触发原因与实际执行策略分别记录。 */
|
|
1046
1069
|
const RecoveryTriggerSchema = zod.z.enum([
|
|
1047
1070
|
"error",
|
|
1048
1071
|
"manual",
|
|
1049
|
-
"visibility"
|
|
1072
|
+
"visibility",
|
|
1073
|
+
"stall",
|
|
1074
|
+
"startup_timeout"
|
|
1050
1075
|
]);
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1076
|
+
const base = {
|
|
1077
|
+
sessionId: zod.z.string().min(1),
|
|
1053
1078
|
recoveryId: zod.z.number().int().positive(),
|
|
1054
1079
|
strategy: RecoveryStrategySchema,
|
|
1055
1080
|
trigger: RecoveryTriggerSchema,
|
|
1056
|
-
|
|
1057
|
-
attempt: zod.z.number().int().positive(),
|
|
1058
|
-
/** 该策略允许的最大恢复轮次。 */
|
|
1081
|
+
attempt: zod.z.number().int().nonnegative(),
|
|
1059
1082
|
maxAttempts: zod.z.number().int().positive(),
|
|
1060
|
-
/**
|
|
1083
|
+
/** 从 detected 起,源端用单调时钟测量的毫秒数。 */
|
|
1084
|
+
elapsedMs: zod.z.number().finite().nonnegative(),
|
|
1061
1085
|
reason: ErrorCodeSchema.optional()
|
|
1062
1086
|
};
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
* 混入检测或验证阶段,避免上报端把“正在验证”误读成“已经恢复”。
|
|
1069
|
-
*/
|
|
1087
|
+
const issued = {
|
|
1088
|
+
...base,
|
|
1089
|
+
attempt: zod.z.number().int().positive()
|
|
1090
|
+
};
|
|
1091
|
+
/** 统一恢复的单条事实;取消不伪报成功,跨会话归属不依赖相邻事件猜测。 */
|
|
1070
1092
|
const RecoveryPayloadSchema = zod.z.discriminatedUnion("phase", [
|
|
1071
1093
|
zod.z.object({
|
|
1072
|
-
...
|
|
1094
|
+
...base,
|
|
1073
1095
|
phase: zod.z.literal("detected")
|
|
1074
1096
|
}).strict(),
|
|
1075
1097
|
zod.z.object({
|
|
1076
|
-
...
|
|
1098
|
+
...base,
|
|
1099
|
+
phase: zod.z.literal("backoff")
|
|
1100
|
+
}).strict(),
|
|
1101
|
+
zod.z.object({
|
|
1102
|
+
...issued,
|
|
1077
1103
|
phase: zod.z.literal("attempting")
|
|
1078
1104
|
}).strict(),
|
|
1079
1105
|
zod.z.object({
|
|
1080
|
-
...
|
|
1106
|
+
...issued,
|
|
1081
1107
|
phase: zod.z.literal("validating")
|
|
1082
1108
|
}).strict(),
|
|
1083
1109
|
zod.z.object({
|
|
1084
|
-
...
|
|
1110
|
+
...issued,
|
|
1085
1111
|
phase: zod.z.literal("recovered"),
|
|
1086
1112
|
validatedBy: zod.z.literal("playing_position_advance")
|
|
1087
1113
|
}).strict(),
|
|
1088
1114
|
zod.z.object({
|
|
1089
|
-
...
|
|
1115
|
+
...base,
|
|
1090
1116
|
phase: zod.z.literal("failed"),
|
|
1091
1117
|
outcome: zod.z.enum(["timeout", "attempts_exhausted"])
|
|
1092
1118
|
}).strict(),
|
|
1093
1119
|
zod.z.object({
|
|
1094
|
-
...
|
|
1120
|
+
...base,
|
|
1095
1121
|
phase: zod.z.literal("cancelled"),
|
|
1096
1122
|
outcome: zod.z.enum([
|
|
1097
1123
|
"source_changed",
|
|
1098
1124
|
"destroyed",
|
|
1099
|
-
"superseded"
|
|
1125
|
+
"superseded",
|
|
1126
|
+
"user_paused",
|
|
1127
|
+
"natural_recovery"
|
|
1100
1128
|
])
|
|
1101
1129
|
}).strict()
|
|
1130
|
+
]).refine((value) => value.attempt <= value.maxAttempts, {
|
|
1131
|
+
message: "attempt exceeds episode budget",
|
|
1132
|
+
path: ["attempt"]
|
|
1133
|
+
});
|
|
1134
|
+
//#endregion
|
|
1135
|
+
//#region src/events.ts
|
|
1136
|
+
/**
|
|
1137
|
+
* 清晰度档位。`level` 是索引,传给 `setQuality` 命令用。
|
|
1138
|
+
*/
|
|
1139
|
+
const QualityLevelSchema = zod.z.object({
|
|
1140
|
+
level: zod.z.number(),
|
|
1141
|
+
label: zod.z.string().optional(),
|
|
1142
|
+
height: zod.z.number().optional(),
|
|
1143
|
+
bitrate: zod.z.number().optional()
|
|
1144
|
+
});
|
|
1145
|
+
/**
|
|
1146
|
+
* 可用字幕轨(player 加载后暴露给消费方)。`id` 是索引,传给 `setSubtitle` 命令用。
|
|
1147
|
+
*
|
|
1148
|
+
* 和输入侧的 {@link SubtitleTrack}(url/content 两种 `mode`)分开:那个是**消费方喂进来**的
|
|
1149
|
+
* 原始字幕描述,这个是 player **加载后回报**的、可切换的轨道清单(同 QualityLevel 之于 quality)。
|
|
1150
|
+
*/
|
|
1151
|
+
const SubtitleTrackInfoSchema = zod.z.object({
|
|
1152
|
+
/** 索引(= source.subtitles 数组下标),`setSubtitle({ id })` 用它引用轨道 */
|
|
1153
|
+
id: zod.z.number(),
|
|
1154
|
+
/** BCP-47,如 `'en'` / `'zh-CN'` / `'th'` */
|
|
1155
|
+
locale: zod.z.string(),
|
|
1156
|
+
/** 展示名,如 `'English'` / `'中文'` */
|
|
1157
|
+
label: zod.z.string()
|
|
1158
|
+
});
|
|
1159
|
+
/**
|
|
1160
|
+
* 「现在能不能正常出画面」的原因枚举({@link PlayerEventSchema} 的 `playablechange`)。
|
|
1161
|
+
*
|
|
1162
|
+
* 优先级(高的压低的,同时命中时报最严重的那个):
|
|
1163
|
+
* `error` > `frame_disconnected` > `autoplay_blocked` > `reconnecting` > `stalled`
|
|
1164
|
+
* > `buffering` > `initializing` > `degraded` > `ok`
|
|
1165
|
+
*
|
|
1166
|
+
* **命名**:枚举值用 `snake_case`,与事件名(全小写连写)是两套命名空间 ——
|
|
1167
|
+
* 事件名对齐 HTML5 媒体事件,枚举值是 payload 里的数据,`autoplay_blocked`
|
|
1168
|
+
* 比 `autoplayblocked` 好读。既有的 `phase: 'start' | 'end'` 是单词故看不出区别。
|
|
1169
|
+
*/
|
|
1170
|
+
const PlayableReasonSchema = zod.z.enum([
|
|
1171
|
+
"ok",
|
|
1172
|
+
"initializing",
|
|
1173
|
+
"buffering",
|
|
1174
|
+
"stalled",
|
|
1175
|
+
"reconnecting",
|
|
1176
|
+
"autoplay_blocked",
|
|
1177
|
+
"error",
|
|
1178
|
+
"frame_disconnected",
|
|
1179
|
+
"degraded"
|
|
1102
1180
|
]);
|
|
1103
1181
|
/**
|
|
1104
1182
|
* 事件。player → host(iframe 模式),或 player-core → 消费方(inline 模式)。
|
|
@@ -1110,6 +1188,10 @@ const RecoveryPayloadSchema = zod.z.discriminatedUnion("phase", [
|
|
|
1110
1188
|
* 权威来源:docs/protocol/events.md + ARCHITECTURE.md § 8.4。
|
|
1111
1189
|
*/
|
|
1112
1190
|
const PlayerEventSchema = zod.z.discriminatedUnion("event", [
|
|
1191
|
+
zod.z.object({
|
|
1192
|
+
event: zod.z.literal("pagefullscreenchange"),
|
|
1193
|
+
payload: PageFullscreenStateSchema
|
|
1194
|
+
}),
|
|
1113
1195
|
zod.z.object({
|
|
1114
1196
|
event: zod.z.literal("sourceroute"),
|
|
1115
1197
|
payload: SourceRoutePayloadSchema
|
|
@@ -1200,43 +1282,6 @@ id: zod.z.number().nullable() })
|
|
|
1200
1282
|
event: zod.z.literal("autoplayblocked"),
|
|
1201
1283
|
payload: zod.z.object({})
|
|
1202
1284
|
}),
|
|
1203
|
-
zod.z.object({
|
|
1204
|
-
event: zod.z.literal("reconnectstart"),
|
|
1205
|
-
payload: zod.z.object({
|
|
1206
|
-
attempt: zod.z.number(),
|
|
1207
|
-
maxAttempts: zod.z.number(),
|
|
1208
|
-
/**
|
|
1209
|
-
* 触发这一轮重连的**契约错误码**;手动 `reconnect()` 时缺省(没有触发它的错误)。
|
|
1210
|
-
*
|
|
1211
|
-
* ⚠️ **这里曾经是 `z.string()`,而那让它在服务端聚合不了**(ADR-069)——
|
|
1212
|
-
* 消费方拿到的类型是 `string`,`switch` 不了、也没有任何东西挡住将来漂成别的写法。
|
|
1213
|
-
* **而源头从来就是一个契约错误码**:`plugins/reconnect.ts` 传的是
|
|
1214
|
-
* `mapXgplayerError(err).code`,没有第二个发射点。
|
|
1215
|
-
*
|
|
1216
|
-
* **复用 `ErrorCodeSchema`,不新起一套「重连原因」枚举** —— 理由同 ADR-062 ③:
|
|
1217
|
-
* 两套表会让同一条内核错误在 `error` 和 `reconnectstart` 两条通道上给出**互相矛盾**的分类,
|
|
1218
|
-
* 而那种漂移没人查得出来。
|
|
1219
|
-
*
|
|
1220
|
-
* ⚠️ **不要照着「今天实际只会出现哪几个码」去收窄。** 那要复刻
|
|
1221
|
-
* `ReconnectPlugin` 的两道过滤(`retryable === true` 且 `category !== 'media'`),
|
|
1222
|
-
* 而那两道闸是**实现细节**,改一行就和契约对不上了 —— 那正是第二份会漂的副本。
|
|
1223
|
-
*/
|
|
1224
|
-
reason: ErrorCodeSchema.optional(),
|
|
1225
|
-
nextDelayMs: zod.z.number().optional()
|
|
1226
|
-
})
|
|
1227
|
-
}),
|
|
1228
|
-
zod.z.object({
|
|
1229
|
-
event: zod.z.literal("reconnectsuccess"),
|
|
1230
|
-
payload: zod.z.object({ attempts: zod.z.number() })
|
|
1231
|
-
}),
|
|
1232
|
-
zod.z.object({
|
|
1233
|
-
event: zod.z.literal("reconnectfailed"),
|
|
1234
|
-
payload: zod.z.object({
|
|
1235
|
-
attempts: zod.z.number(),
|
|
1236
|
-
/** 最后一轮的触发错误码。语义与取值同 `reconnectstart.reason`(ADR-069) */
|
|
1237
|
-
reason: ErrorCodeSchema.optional()
|
|
1238
|
-
})
|
|
1239
|
-
}),
|
|
1240
1285
|
zod.z.object({
|
|
1241
1286
|
event: zod.z.literal("recovery"),
|
|
1242
1287
|
payload: RecoveryPayloadSchema
|
|
@@ -1313,7 +1358,15 @@ id: zod.z.number().nullable() })
|
|
|
1313
1358
|
*(正常启动,该等),握手失败降级后为 `false`(连不上了,该露重试入口)。
|
|
1314
1359
|
* 两个阶段共用一个 reason,靠这个字段区分 —— 这正是它存在的意义。
|
|
1315
1360
|
*/
|
|
1316
|
-
recoverable: zod.z.boolean()
|
|
1361
|
+
recoverable: zod.z.boolean(),
|
|
1362
|
+
/** 下一步动作能力;宿主无须按错误类型自行推断。 */
|
|
1363
|
+
action: zod.z.enum([
|
|
1364
|
+
"retry",
|
|
1365
|
+
"play",
|
|
1366
|
+
"replace-source",
|
|
1367
|
+
"recreate-frame",
|
|
1368
|
+
"none"
|
|
1369
|
+
]).optional()
|
|
1317
1370
|
})
|
|
1318
1371
|
}),
|
|
1319
1372
|
zod.z.object({
|
|
@@ -1457,6 +1510,28 @@ function toErrorEvent(err) {
|
|
|
1457
1510
|
};
|
|
1458
1511
|
}
|
|
1459
1512
|
//#endregion
|
|
1513
|
+
//#region src/delivery.ts
|
|
1514
|
+
/**
|
|
1515
|
+
* 一次已送达播放器事件的生产端证据。
|
|
1516
|
+
*
|
|
1517
|
+
* `sequence` 只在同一个 `producerSessionId` 内严格递增;`deliveryId` 也只承诺在
|
|
1518
|
+
* 该会话内唯一。`occurredAtMs` 是生产端的墙钟,不是宿主接收时间,不能跨设备相减。
|
|
1519
|
+
* 消费者可用 `(producerSessionId, sequence)` 排序、用
|
|
1520
|
+
* `(producerSessionId, deliveryId)` 去重,并拒绝旧会话的迟到事件。
|
|
1521
|
+
*/
|
|
1522
|
+
const DeliveredPlayerEventSchema = zod.z.object({
|
|
1523
|
+
/** 未变形的公开业务事件;旧 raw 出口继续单独交付它。 */
|
|
1524
|
+
event: PlayerEventSchema,
|
|
1525
|
+
/** 每次 player/frame producer 生命周期唯一的会话标识。 */
|
|
1526
|
+
producerSessionId: zod.z.string().min(1),
|
|
1527
|
+
/** 在该 producer session 内唯一且重传时保持稳定的投递标识。 */
|
|
1528
|
+
deliveryId: zod.z.string().min(1),
|
|
1529
|
+
/** 生产端创建该事件时读取的 Unix ms 墙钟。 */
|
|
1530
|
+
occurredAtMs: zod.z.number().finite(),
|
|
1531
|
+
/** 在该 producer session 内从 1 开始严格递增的序号。 */
|
|
1532
|
+
sequence: zod.z.number().int().positive()
|
|
1533
|
+
});
|
|
1534
|
+
//#endregion
|
|
1460
1535
|
//#region src/methods.ts
|
|
1461
1536
|
/**
|
|
1462
1537
|
* 命令。host → iframe(iframe 模式),或消费方 → player-core(inline 模式)。
|
|
@@ -1527,10 +1602,8 @@ const CommandSchema = zod.z.discriminatedUnion("method", [
|
|
|
1527
1602
|
params: zod.z.object({ source: MediaSourceSchema })
|
|
1528
1603
|
}),
|
|
1529
1604
|
zod.z.object({
|
|
1530
|
-
method: zod.z.literal("
|
|
1531
|
-
params: zod.z.object({
|
|
1532
|
-
/** true = 把重连计数清零,重新获得完整的 maxRetries 次机会 */
|
|
1533
|
-
resetCounter: zod.z.boolean().optional() }).optional()
|
|
1605
|
+
method: zod.z.literal("retry"),
|
|
1606
|
+
params: zod.z.object({}).strict().optional()
|
|
1534
1607
|
}),
|
|
1535
1608
|
zod.z.object({
|
|
1536
1609
|
method: zod.z.literal("pushDanmaku"),
|
|
@@ -1566,7 +1639,7 @@ resetCounter: zod.z.boolean().optional() }).optional()
|
|
|
1566
1639
|
* 唯一来源是 contract-version.json(ADR-094);只在真实协议变化时升级。
|
|
1567
1640
|
* npm 包版本同步不能改变握手与 envelope 的版本。
|
|
1568
1641
|
*/
|
|
1569
|
-
const CONTRACT_VERSION = "
|
|
1642
|
+
const CONTRACT_VERSION = "2.0.0";
|
|
1570
1643
|
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
1571
1644
|
/**
|
|
1572
1645
|
* 解析 semver 字符串。只接受严格的 `major.minor.patch`,
|
|
@@ -1657,8 +1730,18 @@ function createEnvelopeSchema(type, payload) {
|
|
|
1657
1730
|
}
|
|
1658
1731
|
/** 命令包裹(host → iframe) */
|
|
1659
1732
|
const CommandEnvelopeSchema = createEnvelopeSchema("command", CommandSchema);
|
|
1660
|
-
|
|
1661
|
-
|
|
1733
|
+
const LegacyEventEnvelopeSchema = createEnvelopeSchema("event", PlayerEventSchema).extend({
|
|
1734
|
+
/** Older iframe builds omit all delivery evidence. */
|
|
1735
|
+
producerSessionId: zod.z.undefined().optional(),
|
|
1736
|
+
sequence: zod.z.undefined().optional()
|
|
1737
|
+
});
|
|
1738
|
+
const DeliveredEventEnvelopeSchema = createEnvelopeSchema("event", PlayerEventSchema).extend({
|
|
1739
|
+
/** Producer-side delivery evidence is atomic: both fields are required together. */
|
|
1740
|
+
producerSessionId: zod.z.string().min(1),
|
|
1741
|
+
sequence: zod.z.number().int().positive()
|
|
1742
|
+
});
|
|
1743
|
+
/** 事件包裹(iframe → host):旧包省略证据,新包同时携带 session 和 sequence。 */
|
|
1744
|
+
const EventEnvelopeSchema = zod.z.union([LegacyEventEnvelopeSchema, DeliveredEventEnvelopeSchema]);
|
|
1662
1745
|
/**
|
|
1663
1746
|
* 响应包裹(iframe → host)。命令的结果一律是 void——
|
|
1664
1747
|
* 播放状态通过事件回来,不塞在 response 里。
|
|
@@ -1669,10 +1752,10 @@ const ErrorEnvelopeSchema = createEnvelopeSchema("error", PlayerErrorSchema);
|
|
|
1669
1752
|
/**
|
|
1670
1753
|
* 任意包裹。收到消息时先用它 parse,再按 `type` 分支。
|
|
1671
1754
|
*
|
|
1672
|
-
*
|
|
1673
|
-
*
|
|
1755
|
+
* event 分支自身要区分旧包和带 delivery evidence 的新包,因此这里用 union。
|
|
1756
|
+
* 每个分支的 `type` 仍是具体字面量,TypeScript 仍可按 `type` 窄化。
|
|
1674
1757
|
*/
|
|
1675
|
-
const EnvelopeSchema = zod.z.
|
|
1758
|
+
const EnvelopeSchema = zod.z.union([
|
|
1676
1759
|
CommandEnvelopeSchema,
|
|
1677
1760
|
EventEnvelopeSchema,
|
|
1678
1761
|
ResponseEnvelopeSchema,
|
|
@@ -1700,6 +1783,13 @@ function commandEnvelope(payload, meta) {
|
|
|
1700
1783
|
}
|
|
1701
1784
|
/** 构造事件包裹(iframe → host) */
|
|
1702
1785
|
function eventEnvelope(payload, meta) {
|
|
1786
|
+
if (meta.producerSessionId !== void 0 && meta.sequence !== void 0) return {
|
|
1787
|
+
...envelopeBase(meta),
|
|
1788
|
+
type: "event",
|
|
1789
|
+
payload,
|
|
1790
|
+
producerSessionId: meta.producerSessionId,
|
|
1791
|
+
sequence: meta.sequence
|
|
1792
|
+
};
|
|
1703
1793
|
return {
|
|
1704
1794
|
...envelopeBase(meta),
|
|
1705
1795
|
type: "event",
|
|
@@ -1823,8 +1913,10 @@ function resolvePreset(preset, config) {
|
|
|
1823
1913
|
exports.CONTRACT_VERSION = CONTRACT_VERSION;
|
|
1824
1914
|
exports.CommandEnvelopeSchema = CommandEnvelopeSchema;
|
|
1825
1915
|
exports.CommandSchema = CommandSchema;
|
|
1916
|
+
exports.ControlVisibilityConfigSchema = ControlVisibilityConfigSchema;
|
|
1826
1917
|
exports.DanmakuConfigSchema = DanmakuConfigSchema;
|
|
1827
1918
|
exports.DanmakuItemSchema = DanmakuItemSchema;
|
|
1919
|
+
exports.DeliveredPlayerEventSchema = DeliveredPlayerEventSchema;
|
|
1828
1920
|
exports.ERROR_META = ERROR_META;
|
|
1829
1921
|
exports.EnvelopeSchema = EnvelopeSchema;
|
|
1830
1922
|
exports.EnvelopeTypeSchema = EnvelopeTypeSchema;
|
|
@@ -1843,6 +1935,9 @@ exports.MediaTypeSchema = MediaTypeSchema;
|
|
|
1843
1935
|
exports.MultiSourceObjectSchema = MultiSourceObjectSchema;
|
|
1844
1936
|
exports.PLAYER_DESTROYED_MESSAGE = PLAYER_DESTROYED_MESSAGE;
|
|
1845
1937
|
exports.PRESETS = PRESETS;
|
|
1938
|
+
exports.PageFullscreenControlSchema = PageFullscreenControlSchema;
|
|
1939
|
+
exports.PageFullscreenMessageSchema = PageFullscreenMessageSchema;
|
|
1940
|
+
exports.PageFullscreenStateSchema = PageFullscreenStateSchema;
|
|
1846
1941
|
exports.PauseImageConfigSchema = PauseImageConfigSchema;
|
|
1847
1942
|
exports.PlayableReasonSchema = PlayableReasonSchema;
|
|
1848
1943
|
exports.PlaybackContextSchema = PlaybackContextSchema;
|