@video-lab/protocol 3.1.0 → 4.0.1
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 +35 -15
- package/dist/index.cjs +476 -152
- package/dist/index.d.cts +8137 -3812
- package/dist/index.d.mts +8137 -3812
- package/dist/index.mjs +459 -153
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,18 +25,17 @@ const PosterConfigSchema = zod.z.union([zod.z.string(), zod.z.object({
|
|
|
25
25
|
]).default("cover").optional()
|
|
26
26
|
})]);
|
|
27
27
|
/**
|
|
28
|
-
* 暂停时盖在画面中央的静态图(ADR-043 /
|
|
28
|
+
* 暂停时盖在画面中央的静态图(ADR-043 / ADR-126)。
|
|
29
29
|
*
|
|
30
30
|
* 形状**刻意与 {@link PosterConfigSchema} 对齐**(同为 `string | { url, fit, … }`),
|
|
31
31
|
* 消费方不用学两套。
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* 默认图片在 iframe 内渲染;静态 iframe 由此也能使用。框架组件传入自定义
|
|
34
|
+
* render / slot 时由宿主 DOM 渲染,iframe 配置省略本字段(ADR-127)。
|
|
35
35
|
*
|
|
36
|
-
* **边界(ADR-025 已判的那条线)
|
|
36
|
+
* **边界(ADR-025 已判的那条线)**:静态图与继续播放按钮进 SDK —— 可序列化、无调度逻辑,
|
|
37
37
|
* 与 `poster` 同性质;**带倒计时 / 跳转 / 推荐列表的不进** —— 那是业务调度,归团队层。
|
|
38
|
-
*
|
|
39
|
-
* 过不了 postMessage,所以三条 iframe 路只有本字段有效。
|
|
38
|
+
* 四个框架组件另有具名自定义视觉入口;函数和节点不进入本协议。
|
|
40
39
|
*/
|
|
41
40
|
const PauseImageConfigSchema = zod.z.union([zod.z.string(), zod.z.object({
|
|
42
41
|
url: zod.z.string(),
|
|
@@ -52,10 +51,13 @@ const PauseImageConfigSchema = zod.z.union([zod.z.string(), zod.z.object({
|
|
|
52
51
|
"cover",
|
|
53
52
|
"contain",
|
|
54
53
|
"fill"
|
|
55
|
-
]).optional()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
]).optional()
|
|
55
|
+
}).strict()]);
|
|
56
|
+
/** 默认错误层动作按钮的配色;跨 iframe 边界传递字面 CSS 颜色值。 */
|
|
57
|
+
const ErrorButtonColorsSchema = zod.z.object({
|
|
58
|
+
background: zod.z.string().min(1).optional(),
|
|
59
|
+
foreground: zod.z.string().min(1).optional()
|
|
60
|
+
});
|
|
59
61
|
/** 字幕轨 */
|
|
60
62
|
const SubtitleTrackSchema = zod.z.discriminatedUnion("mode", [zod.z.object({
|
|
61
63
|
mode: zod.z.literal("url"),
|
|
@@ -158,7 +160,7 @@ const SingleSourceObjectSchema = zod.z.object({
|
|
|
158
160
|
/**
|
|
159
161
|
* 多源对象。直播 FLV + HLS 双路兜底的标准形态。
|
|
160
162
|
*
|
|
161
|
-
* ⚠️ FLV
|
|
163
|
+
* ⚠️ 当前 SDK 收到 FLV 单源且运行在 iOS / 微信 / UC / 夸克时会抛 `E_MEDIA_NOT_SUPPORTED`,
|
|
162
164
|
* **FLV 永远和 HLS 一起放进 sources 数组**。
|
|
163
165
|
*/
|
|
164
166
|
const MultiSourceObjectSchema = zod.z.object({
|
|
@@ -169,9 +171,9 @@ const MultiSourceObjectSchema = zod.z.object({
|
|
|
169
171
|
* 媒体源。三种写法:URL 字符串(最简)、单源对象、多源对象。
|
|
170
172
|
*
|
|
171
173
|
* **这是 wire schema——只含可 JSON 序列化的字段**。
|
|
172
|
-
* ⛔ **`source.onBeforeRequest`
|
|
173
|
-
*
|
|
174
|
-
* 认证走签名 URL + 长有效期(ADR-022)
|
|
174
|
+
* ⛔ **`source.onBeforeRequest` 与其类型已在 4.0.0 移除(ADR-057)** ——
|
|
175
|
+
* 它从来不在任何 Schema 里,player-core 里也没有消费点(#332)。
|
|
176
|
+
* 认证走签名 URL + 长有效期(ADR-022)。
|
|
175
177
|
*/
|
|
176
178
|
const MediaSourceSchema = zod.z.union([
|
|
177
179
|
zod.z.string(),
|
|
@@ -183,18 +185,18 @@ const LocaleConfigSchema = zod.z.union([zod.z.string(), zod.z.object({
|
|
|
183
185
|
locale: zod.z.string(),
|
|
184
186
|
fallbackLocales: zod.z.array(zod.z.string()).optional(),
|
|
185
187
|
/**
|
|
186
|
-
*
|
|
187
|
-
*(loading / error.* / retry /
|
|
188
|
+
* 翻译表。覆盖层内置中 / 英默认文案,消费方可从这里逐 key 覆盖
|
|
189
|
+
*(loading / loading.<reason> / play / error.* / retry / reconnect)。
|
|
188
190
|
* 消费面用 `resolveLocaleMessages` 把它解析成扁平表,再把解析好的**字符串**
|
|
189
|
-
*
|
|
191
|
+
* 传给覆盖层元素;标准覆盖层缺 key 时使用中 / 英默认值。
|
|
190
192
|
*
|
|
191
193
|
* **两个命名空间,同一张表**:
|
|
192
|
-
* - 覆盖层:`loading` / `retry` / `
|
|
194
|
+
* - 覆盖层:`loading.*` / `play` / `retry` / `reconnect` / `error.<CODE>` —— 内置中 / 英
|
|
193
195
|
* - 播放内核自带控件:`controls.*` —— 内置中 / 英,你传的会**覆盖**(#208 / ADR-051)
|
|
194
196
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
197
|
+
* xgplayer 的内部槽位只有中 / 英两档(`zh-CN` → 中文,其他 → 英文,见 player-core 的 `toXgLang`),
|
|
198
|
+
* 但显示语言不限于这两种:`vi-VN` 可使用官方语言包,第三语言的控件文案写入
|
|
199
|
+
* 当前实例的英文槽位,不污染其他播放器。
|
|
198
200
|
* `controls.*` 的 key 全集见 USER-GUIDE § 9.4;**表外的静默忽略**。
|
|
199
201
|
*
|
|
200
202
|
* ⚠️ 这段从前写着「控件不读 messages」—— 那是 #208 修掉的现状,别照着旧描述下结论。
|
|
@@ -202,7 +204,7 @@ const LocaleConfigSchema = zod.z.union([zod.z.string(), zod.z.object({
|
|
|
202
204
|
messages: zod.z.record(zod.z.string(), zod.z.record(zod.z.string(), zod.z.string())).optional()
|
|
203
205
|
})]);
|
|
204
206
|
/**
|
|
205
|
-
* 弹幕单条(streaming push 用)
|
|
207
|
+
* 弹幕单条(streaming push 用)。长期取舍见 ADR-028。
|
|
206
208
|
*
|
|
207
209
|
* 直播场景:业务从自己的数据源(WebSocket / 轮询,SDK 不参与)拿到一条,
|
|
208
210
|
* 经 `pushDanmaku` 命令喂进来,到点即显(不绑视频时间)。
|
|
@@ -239,7 +241,7 @@ const DanmakuItemSchema = zod.z.object({
|
|
|
239
241
|
start: zod.z.number().nonnegative().optional()
|
|
240
242
|
});
|
|
241
243
|
/**
|
|
242
|
-
* 弹幕配置(PlayerConfig.danmaku,默认关闭)
|
|
244
|
+
* 弹幕配置(PlayerConfig.danmaku,默认关闭)。长期取舍见 ADR-028。
|
|
243
245
|
*
|
|
244
246
|
* **两种模式**:`streaming`(直播,`pushDanmaku` 逐条)/ `preload`(点播,挂载时整包给,
|
|
245
247
|
* 引擎按每条的 `start` 排期,见 #275)。ADR-028 当初只做 streaming,preload 是它说的「后续 minor」。
|
|
@@ -301,7 +303,7 @@ const DanmakuConfigSchema = zod.z.object({
|
|
|
301
303
|
*
|
|
302
304
|
* 定义在这里而不是 presets.ts,是为了让 PlayerConfig 能引用它而不产生循环依赖。
|
|
303
305
|
*/
|
|
304
|
-
const PresetNameSchema = zod.z.enum(["
|
|
306
|
+
const PresetNameSchema = zod.z.enum(["ambient-preview"]);
|
|
305
307
|
/** 内置控制栏的初始化显示策略。false 隐藏入口;true/省略仍遵守平台能力。 */
|
|
306
308
|
const ControlVisibilityConfigSchema = zod.z.object({
|
|
307
309
|
play: zod.z.boolean().optional(),
|
|
@@ -370,8 +372,12 @@ const PlayerConfigSchema = zod.z.object({
|
|
|
370
372
|
controls: zod.z.boolean().optional(),
|
|
371
373
|
/** 初始化时是否显示默认错误覆盖层。默认开启;关闭不影响错误事件和恢复。 */
|
|
372
374
|
showErrorOverlay: zod.z.boolean().optional(),
|
|
373
|
-
/**
|
|
374
|
-
|
|
375
|
+
/** 是否显示 SDK 默认运行时 Loading;false 仅隐藏内置视觉/ARIA,不影响状态事件和恢复。 */
|
|
376
|
+
showDefaultLoadingOverlay: zod.z.boolean().optional(),
|
|
377
|
+
/** 在 SDK 默认 Loading 转圈下显示当前状态文案;默认 false,随默认 Loading 一起隐藏。 */
|
|
378
|
+
showLoadingText: zod.z.boolean().optional(),
|
|
379
|
+
/** 默认错误动作按钮配色;不改变状态判断或动作。 */
|
|
380
|
+
errorButtonColors: ErrorButtonColorsSchema.optional(),
|
|
375
381
|
/** 独立隐藏内置控件;只在创建时生效,修改后由宿主显式重建。 */
|
|
376
382
|
controlVisibility: ControlVisibilityConfigSchema.optional(),
|
|
377
383
|
/** 是否响应用户交互。首页预览卡片用 false,点击透传给外层卡片 */
|
|
@@ -399,6 +405,26 @@ const PlayerConfigSchema = zod.z.object({
|
|
|
399
405
|
debug: zod.z.boolean().optional()
|
|
400
406
|
});
|
|
401
407
|
//#endregion
|
|
408
|
+
//#region src/delivery.ts
|
|
409
|
+
/**
|
|
410
|
+
* 一次播放器事件的生产端投递证据。
|
|
411
|
+
*
|
|
412
|
+
* `sequence` 只在同一个 `producerSessionId` 内严格递增;`deliveryId` 也只承诺在
|
|
413
|
+
* 该会话内唯一。`occurredAtMs` 是生产端的墙钟,不是宿主接收时间,不能跨设备相减。
|
|
414
|
+
* 消费者可用 `(producerSessionId, sequence)` 排序、用
|
|
415
|
+
* `(producerSessionId, deliveryId)` 去重,并拒绝旧会话的迟到事件。
|
|
416
|
+
*/
|
|
417
|
+
const PlayerEventDeliverySchema = zod.z.object({
|
|
418
|
+
/** 每次 player/frame producer 生命周期唯一的会话标识。 */
|
|
419
|
+
producerSessionId: zod.z.string().min(1),
|
|
420
|
+
/** 在该 producer session 内唯一且重传时保持稳定的投递标识。 */
|
|
421
|
+
deliveryId: zod.z.string().min(1),
|
|
422
|
+
/** 生产端创建该事件时读取的 Unix ms 墙钟。 */
|
|
423
|
+
occurredAtMs: zod.z.number().finite(),
|
|
424
|
+
/** 在该 producer session 内从 1 开始严格递增的序号。 */
|
|
425
|
+
sequence: zod.z.number().int().positive()
|
|
426
|
+
});
|
|
427
|
+
//#endregion
|
|
402
428
|
//#region src/errors.ts
|
|
403
429
|
/**
|
|
404
430
|
* 错误分类。player-core 把 xgplayer 的内部错误映射到这几类,
|
|
@@ -414,7 +440,7 @@ const ErrorCategorySchema = zod.z.enum([
|
|
|
414
440
|
]);
|
|
415
441
|
/**
|
|
416
442
|
* 全部错误码。命名规则 `E_<CATEGORY>_<SPECIFIC>`。
|
|
417
|
-
*
|
|
443
|
+
* 权威来源:本文件;人类可读说明见 docs/protocol/errors.md。
|
|
418
444
|
*
|
|
419
445
|
* **每一个码都必须有真实发射点**(ADR-034)。`player-core/tests/contract-coverage.test.ts`
|
|
420
446
|
* 静态扫描全仓库生产代码强制这条 —— 加码不加实现,测试直接红。
|
|
@@ -566,6 +592,19 @@ const ErrorCauseSchema = zod.z.object({
|
|
|
566
592
|
]));
|
|
567
593
|
/** `String(x)` 的截断上限 —— cause 是旁路信息,不该把 envelope 撑大 */
|
|
568
594
|
const CAUSE_MESSAGE_MAX = 500;
|
|
595
|
+
/** 匹配字符串里出现的 http(s) URL,用于把 query/fragment 剥掉,只留 origin+pathname */
|
|
596
|
+
const URL_PATTERN = /https?:\/\/\S+/g;
|
|
597
|
+
/** 剥掉字符串里每个 URL 的 query 串和 fragment —— 签名 token 常年住在 query 里 */
|
|
598
|
+
function stripUrlQuery(value) {
|
|
599
|
+
return value.replace(URL_PATTERN, (match) => {
|
|
600
|
+
const cut = match.search(/[?#]/);
|
|
601
|
+
return cut === -1 ? match : match.slice(0, cut);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
/** 对可能非字符串的值套用 {@link stripUrlQuery};非字符串原样返回 */
|
|
605
|
+
function stripUrlQueryIfString(value) {
|
|
606
|
+
return typeof value === "string" ? stripUrlQuery(value) : value;
|
|
607
|
+
}
|
|
569
608
|
function readNumber(source, key) {
|
|
570
609
|
const v = source[key];
|
|
571
610
|
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
@@ -587,20 +626,21 @@ function readNumber(source, key) {
|
|
|
587
626
|
function serializeCause(cause) {
|
|
588
627
|
if (cause === null || typeof cause !== "object") return {
|
|
589
628
|
name: "Unknown",
|
|
590
|
-
message: String(cause).slice(0, CAUSE_MESSAGE_MAX)
|
|
629
|
+
message: stripUrlQuery(String(cause)).slice(0, CAUSE_MESSAGE_MAX)
|
|
591
630
|
};
|
|
592
631
|
const obj = cause;
|
|
593
632
|
const media = obj.mediaError ?? void 0;
|
|
594
|
-
const name = typeof obj.name === "string" ? obj.name : media ? "MediaError" : typeof obj.errorType === "string" ? obj.errorType : "Unknown";
|
|
633
|
+
const name = stripUrlQueryIfString(typeof obj.name === "string" ? obj.name : media ? "MediaError" : typeof obj.errorType === "string" ? obj.errorType : "Unknown");
|
|
595
634
|
const rawMessage = typeof obj.message === "string" ? obj.message : typeof media?.message === "string" ? media.message : String(cause);
|
|
596
635
|
const code = readNumber(obj, "code") ?? (media ? readNumber(media, "code") : void 0);
|
|
597
636
|
const status = readNumber(obj, "status") ?? readNumber(obj, "httpCode");
|
|
598
637
|
const extras = {};
|
|
599
|
-
for (const [k, v] of Object.entries(obj)) if (typeof v === "string"
|
|
638
|
+
for (const [k, v] of Object.entries(obj)) if (typeof v === "string") extras[k] = stripUrlQuery(v);
|
|
639
|
+
else if (typeof v === "number" || typeof v === "boolean") extras[k] = v;
|
|
600
640
|
return {
|
|
601
641
|
...extras,
|
|
602
642
|
name,
|
|
603
|
-
message: rawMessage.slice(0, CAUSE_MESSAGE_MAX),
|
|
643
|
+
message: stripUrlQuery(rawMessage).slice(0, CAUSE_MESSAGE_MAX),
|
|
604
644
|
...code === void 0 ? {} : { code },
|
|
605
645
|
...status === void 0 ? {} : { status }
|
|
606
646
|
};
|
|
@@ -638,7 +678,7 @@ function makePlayerError(code, message, cause) {
|
|
|
638
678
|
const meta = ERROR_META[code];
|
|
639
679
|
return {
|
|
640
680
|
code,
|
|
641
|
-
message,
|
|
681
|
+
message: stripUrlQuery(message),
|
|
642
682
|
category: meta.category,
|
|
643
683
|
retryable: meta.retryable,
|
|
644
684
|
...cause === void 0 ? {} : { cause: serializeCause(cause) }
|
|
@@ -801,8 +841,9 @@ function normalizeUserAction(raw) {
|
|
|
801
841
|
const r = raw;
|
|
802
842
|
const action = r.action;
|
|
803
843
|
if (typeof action !== "string" || !ALLOWED.has(action)) return null;
|
|
804
|
-
const
|
|
805
|
-
const
|
|
844
|
+
const rawProps = r.props;
|
|
845
|
+
const first = (Array.isArray(rawProps) ? rawProps : [])[0];
|
|
846
|
+
const change = typeof first === "string" ? r : typeof first === "object" && first !== null ? first : rawProps === void 0 && typeof r.prop === "string" ? r : null;
|
|
806
847
|
return {
|
|
807
848
|
action,
|
|
808
849
|
source: typeof r.pluginName === "string" ? r.pluginName : "player",
|
|
@@ -1059,7 +1100,11 @@ zod.z.enum([
|
|
|
1059
1100
|
"failed",
|
|
1060
1101
|
"cancelled"
|
|
1061
1102
|
]);
|
|
1062
|
-
/**
|
|
1103
|
+
/**
|
|
1104
|
+
* 策略变化共享 episode,不创建新的预算。
|
|
1105
|
+
* 报的是该次动作**实际执行**的动作:请求时的意图若这一次做不到(如非 hls.js 内核无法做
|
|
1106
|
+
* 媒体修复),按实际动作上报(ADR-109)。
|
|
1107
|
+
*/
|
|
1063
1108
|
const RecoveryStrategySchema = zod.z.enum([
|
|
1064
1109
|
"reconnect",
|
|
1065
1110
|
"media_recovery",
|
|
@@ -1071,7 +1116,8 @@ const RecoveryTriggerSchema = zod.z.enum([
|
|
|
1071
1116
|
"manual",
|
|
1072
1117
|
"visibility",
|
|
1073
1118
|
"stall",
|
|
1074
|
-
"startup_timeout"
|
|
1119
|
+
"startup_timeout",
|
|
1120
|
+
"ended"
|
|
1075
1121
|
]);
|
|
1076
1122
|
const base = {
|
|
1077
1123
|
sessionId: zod.z.string().min(1),
|
|
@@ -1132,6 +1178,50 @@ const RecoveryPayloadSchema = zod.z.discriminatedUnion("phase", [
|
|
|
1132
1178
|
path: ["attempt"]
|
|
1133
1179
|
});
|
|
1134
1180
|
//#endregion
|
|
1181
|
+
//#region src/source-switch.ts
|
|
1182
|
+
/**
|
|
1183
|
+
* 自动授权换源的低频事务事实;只放可安全聚合的闭集枚举与数值。
|
|
1184
|
+
*
|
|
1185
|
+
* @platform static-iframe unsupported:静态 URL 无法传入 `ResolveSource` 函数,
|
|
1186
|
+
* 因而不产生本事务;宿主应在 `error(E_AUTH_EXPIRED)` 后替换整个 iframe。
|
|
1187
|
+
*/
|
|
1188
|
+
const SourceSwitchPayloadSchema = zod.z.discriminatedUnion("phase", [
|
|
1189
|
+
zod.z.object({
|
|
1190
|
+
phase: zod.z.literal("started"),
|
|
1191
|
+
switchId: zod.z.number().int().positive(),
|
|
1192
|
+
trigger: zod.z.literal("auth_expired"),
|
|
1193
|
+
httpStatus: zod.z.union([zod.z.literal(401), zod.z.literal(403)]).optional()
|
|
1194
|
+
}).strict(),
|
|
1195
|
+
zod.z.object({
|
|
1196
|
+
phase: zod.z.literal("succeeded"),
|
|
1197
|
+
switchId: zod.z.number().int().positive(),
|
|
1198
|
+
strategy: zod.z.enum(["hot_load", "rebuild"]),
|
|
1199
|
+
elapsedMs: zod.z.number().nonnegative()
|
|
1200
|
+
}).strict(),
|
|
1201
|
+
zod.z.object({
|
|
1202
|
+
phase: zod.z.literal("failed"),
|
|
1203
|
+
switchId: zod.z.number().int().positive(),
|
|
1204
|
+
reason: zod.z.enum([
|
|
1205
|
+
"resolver_null",
|
|
1206
|
+
"resolver_rejected",
|
|
1207
|
+
"load_rejected",
|
|
1208
|
+
"replacement_error"
|
|
1209
|
+
]),
|
|
1210
|
+
elapsedMs: zod.z.number().nonnegative(),
|
|
1211
|
+
errorCode: ErrorCodeSchema.optional()
|
|
1212
|
+
}).strict(),
|
|
1213
|
+
zod.z.object({
|
|
1214
|
+
phase: zod.z.literal("cancelled"),
|
|
1215
|
+
switchId: zod.z.number().int().positive(),
|
|
1216
|
+
reason: zod.z.enum([
|
|
1217
|
+
"external_source",
|
|
1218
|
+
"destroyed",
|
|
1219
|
+
"unmounted"
|
|
1220
|
+
]),
|
|
1221
|
+
elapsedMs: zod.z.number().nonnegative()
|
|
1222
|
+
}).strict()
|
|
1223
|
+
]);
|
|
1224
|
+
//#endregion
|
|
1135
1225
|
//#region src/events.ts
|
|
1136
1226
|
/**
|
|
1137
1227
|
* 清晰度档位。`level` 是索引,传给 `setQuality` 命令用。
|
|
@@ -1160,7 +1250,7 @@ const SubtitleTrackInfoSchema = zod.z.object({
|
|
|
1160
1250
|
* 「现在能不能正常出画面」的原因枚举({@link PlayerEventSchema} 的 `playablechange`)。
|
|
1161
1251
|
*
|
|
1162
1252
|
* 优先级(高的压低的,同时命中时报最严重的那个):
|
|
1163
|
-
* `error` > `frame_disconnected` > `autoplay_blocked` > `reconnecting` > `stalled`
|
|
1253
|
+
* `error` > `frame_disconnected` > `autoplay_blocked` > `source_switching` > `reconnecting` > `stalled`
|
|
1164
1254
|
* > `buffering` > `initializing` > `degraded` > `ok`
|
|
1165
1255
|
*
|
|
1166
1256
|
* **命名**:枚举值用 `snake_case`,与事件名(全小写连写)是两套命名空间 ——
|
|
@@ -1173,6 +1263,7 @@ const PlayableReasonSchema = zod.z.enum([
|
|
|
1173
1263
|
"buffering",
|
|
1174
1264
|
"stalled",
|
|
1175
1265
|
"reconnecting",
|
|
1266
|
+
"source_switching",
|
|
1176
1267
|
"autoplay_blocked",
|
|
1177
1268
|
"error",
|
|
1178
1269
|
"frame_disconnected",
|
|
@@ -1185,18 +1276,22 @@ const PlayableReasonSchema = zod.z.enum([
|
|
|
1185
1276
|
* 这是 wire 上的名字;消费面各自映射——Vue emit `time-update`,React prop `onTimeUpdate`。
|
|
1186
1277
|
* (团队约定里的 `snake.case` 指的是埋点事件名如 `playback.start`,那是另一套命名空间。)
|
|
1187
1278
|
*
|
|
1188
|
-
*
|
|
1279
|
+
* 权威来源:本 Zod schema;docs/protocol/events.md 是人类可读说明。
|
|
1189
1280
|
*/
|
|
1281
|
+
const eventObject = (shape) => zod.z.object({
|
|
1282
|
+
...shape,
|
|
1283
|
+
delivery: PlayerEventDeliverySchema.optional()
|
|
1284
|
+
});
|
|
1190
1285
|
const PlayerEventSchema = zod.z.discriminatedUnion("event", [
|
|
1191
|
-
|
|
1286
|
+
eventObject({
|
|
1192
1287
|
event: zod.z.literal("pagefullscreenchange"),
|
|
1193
1288
|
payload: PageFullscreenStateSchema
|
|
1194
1289
|
}),
|
|
1195
|
-
|
|
1290
|
+
eventObject({
|
|
1196
1291
|
event: zod.z.literal("sourceroute"),
|
|
1197
1292
|
payload: SourceRoutePayloadSchema
|
|
1198
1293
|
}),
|
|
1199
|
-
|
|
1294
|
+
eventObject({
|
|
1200
1295
|
event: zod.z.literal("ready"),
|
|
1201
1296
|
payload: zod.z.object({
|
|
1202
1297
|
duration: zod.z.number(),
|
|
@@ -1211,19 +1306,19 @@ const PlayerEventSchema = zod.z.discriminatedUnion("event", [
|
|
|
1211
1306
|
subtitles: zod.z.array(SubtitleTrackInfoSchema).optional()
|
|
1212
1307
|
})
|
|
1213
1308
|
}),
|
|
1214
|
-
|
|
1309
|
+
eventObject({
|
|
1215
1310
|
event: zod.z.literal("play"),
|
|
1216
1311
|
payload: zod.z.object({})
|
|
1217
1312
|
}),
|
|
1218
|
-
|
|
1313
|
+
eventObject({
|
|
1219
1314
|
event: zod.z.literal("pause"),
|
|
1220
1315
|
payload: zod.z.object({})
|
|
1221
1316
|
}),
|
|
1222
|
-
|
|
1317
|
+
eventObject({
|
|
1223
1318
|
event: zod.z.literal("ended"),
|
|
1224
1319
|
payload: zod.z.object({})
|
|
1225
1320
|
}),
|
|
1226
|
-
|
|
1321
|
+
eventObject({
|
|
1227
1322
|
event: zod.z.literal("timeupdate"),
|
|
1228
1323
|
/**
|
|
1229
1324
|
* ~250ms 一次。
|
|
@@ -1237,30 +1332,30 @@ const PlayerEventSchema = zod.z.discriminatedUnion("event", [
|
|
|
1237
1332
|
duration: zod.z.number()
|
|
1238
1333
|
})
|
|
1239
1334
|
}),
|
|
1240
|
-
|
|
1335
|
+
eventObject({
|
|
1241
1336
|
event: zod.z.literal("volumechange"),
|
|
1242
1337
|
payload: zod.z.object({
|
|
1243
1338
|
volume: zod.z.number(),
|
|
1244
1339
|
muted: zod.z.boolean()
|
|
1245
1340
|
})
|
|
1246
1341
|
}),
|
|
1247
|
-
|
|
1342
|
+
eventObject({
|
|
1248
1343
|
event: zod.z.literal("seeking"),
|
|
1249
1344
|
payload: zod.z.object({ time: zod.z.number() })
|
|
1250
1345
|
}),
|
|
1251
|
-
|
|
1346
|
+
eventObject({
|
|
1252
1347
|
event: zod.z.literal("seeked"),
|
|
1253
1348
|
payload: zod.z.object({ time: zod.z.number() })
|
|
1254
1349
|
}),
|
|
1255
|
-
|
|
1350
|
+
eventObject({
|
|
1256
1351
|
event: zod.z.literal("waiting"),
|
|
1257
1352
|
payload: zod.z.object({})
|
|
1258
1353
|
}),
|
|
1259
|
-
|
|
1354
|
+
eventObject({
|
|
1260
1355
|
event: zod.z.literal("playing"),
|
|
1261
1356
|
payload: zod.z.object({})
|
|
1262
1357
|
}),
|
|
1263
|
-
|
|
1358
|
+
eventObject({
|
|
1264
1359
|
event: zod.z.literal("qualitychange"),
|
|
1265
1360
|
payload: zod.z.object({
|
|
1266
1361
|
level: zod.z.number(),
|
|
@@ -1268,25 +1363,29 @@ const PlayerEventSchema = zod.z.discriminatedUnion("event", [
|
|
|
1268
1363
|
auto: zod.z.boolean()
|
|
1269
1364
|
})
|
|
1270
1365
|
}),
|
|
1271
|
-
|
|
1366
|
+
eventObject({
|
|
1272
1367
|
event: zod.z.literal("subtitlechange"),
|
|
1273
1368
|
payload: zod.z.object({
|
|
1274
1369
|
/** 当前激活字幕轨 id(= {@link SubtitleTrackInfo} 的 `id`);`null` = 字幕已关闭 */
|
|
1275
1370
|
id: zod.z.number().nullable() })
|
|
1276
1371
|
}),
|
|
1277
|
-
|
|
1372
|
+
eventObject({
|
|
1278
1373
|
event: zod.z.literal("error"),
|
|
1279
1374
|
payload: PlayerErrorSchema
|
|
1280
1375
|
}),
|
|
1281
|
-
|
|
1376
|
+
eventObject({
|
|
1282
1377
|
event: zod.z.literal("autoplayblocked"),
|
|
1283
1378
|
payload: zod.z.object({})
|
|
1284
1379
|
}),
|
|
1285
|
-
|
|
1380
|
+
eventObject({
|
|
1286
1381
|
event: zod.z.literal("recovery"),
|
|
1287
1382
|
payload: RecoveryPayloadSchema
|
|
1288
1383
|
}),
|
|
1289
|
-
|
|
1384
|
+
eventObject({
|
|
1385
|
+
event: zod.z.literal("sourceswitch"),
|
|
1386
|
+
payload: SourceSwitchPayloadSchema
|
|
1387
|
+
}),
|
|
1388
|
+
eventObject({
|
|
1290
1389
|
event: zod.z.literal("compatwarning"),
|
|
1291
1390
|
payload: zod.z.object({
|
|
1292
1391
|
code: WarningCodeSchema,
|
|
@@ -1316,7 +1415,7 @@ id: zod.z.number().nullable() })
|
|
|
1316
1415
|
rejectedEvent: zod.z.string().optional()
|
|
1317
1416
|
})
|
|
1318
1417
|
}),
|
|
1319
|
-
|
|
1418
|
+
eventObject({
|
|
1320
1419
|
event: zod.z.literal("stalled"),
|
|
1321
1420
|
payload: zod.z.object({
|
|
1322
1421
|
phase: zod.z.enum(["start", "end"]),
|
|
@@ -1341,7 +1440,7 @@ id: zod.z.number().nullable() })
|
|
|
1341
1440
|
]).optional()
|
|
1342
1441
|
})
|
|
1343
1442
|
}),
|
|
1344
|
-
|
|
1443
|
+
eventObject({
|
|
1345
1444
|
event: zod.z.literal("playablechange"),
|
|
1346
1445
|
payload: zod.z.object({
|
|
1347
1446
|
/** 能不能正常出画面。消费方只读这一个字段就够 */
|
|
@@ -1369,7 +1468,7 @@ id: zod.z.number().nullable() })
|
|
|
1369
1468
|
]).optional()
|
|
1370
1469
|
})
|
|
1371
1470
|
}),
|
|
1372
|
-
|
|
1471
|
+
eventObject({
|
|
1373
1472
|
event: zod.z.literal("kernelhealth"),
|
|
1374
1473
|
payload: zod.z.object({
|
|
1375
1474
|
/**
|
|
@@ -1409,7 +1508,7 @@ id: zod.z.number().nullable() })
|
|
|
1409
1508
|
detail: zod.z.string()
|
|
1410
1509
|
})
|
|
1411
1510
|
}),
|
|
1412
|
-
|
|
1511
|
+
eventObject({
|
|
1413
1512
|
event: zod.z.literal("audiohealth"),
|
|
1414
1513
|
payload: zod.z.object({
|
|
1415
1514
|
degraded: zod.z.boolean(),
|
|
@@ -1417,7 +1516,7 @@ id: zod.z.number().nullable() })
|
|
|
1417
1516
|
durationMs: zod.z.number()
|
|
1418
1517
|
})
|
|
1419
1518
|
}),
|
|
1420
|
-
|
|
1519
|
+
eventObject({
|
|
1421
1520
|
event: zod.z.literal("bufferhealth"),
|
|
1422
1521
|
payload: zod.z.object({
|
|
1423
1522
|
/**
|
|
@@ -1446,19 +1545,19 @@ id: zod.z.number().nullable() })
|
|
|
1446
1545
|
marginSec: zod.z.number()
|
|
1447
1546
|
})
|
|
1448
1547
|
}),
|
|
1449
|
-
|
|
1548
|
+
eventObject({
|
|
1450
1549
|
event: zod.z.literal("contextchange"),
|
|
1451
1550
|
payload: PlaybackContextSchema
|
|
1452
1551
|
}),
|
|
1453
|
-
|
|
1552
|
+
eventObject({
|
|
1454
1553
|
event: zod.z.literal("firstframe"),
|
|
1455
1554
|
payload: FirstFramePayloadSchema
|
|
1456
1555
|
}),
|
|
1457
|
-
|
|
1556
|
+
eventObject({
|
|
1458
1557
|
event: zod.z.literal("framefreeze"),
|
|
1459
1558
|
payload: FrameFreezePayloadSchema
|
|
1460
1559
|
}),
|
|
1461
|
-
|
|
1560
|
+
eventObject({
|
|
1462
1561
|
event: zod.z.literal("useraction"),
|
|
1463
1562
|
payload: UserActionPayloadSchema
|
|
1464
1563
|
})
|
|
@@ -1510,33 +1609,11 @@ function toErrorEvent(err) {
|
|
|
1510
1609
|
};
|
|
1511
1610
|
}
|
|
1512
1611
|
//#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
|
|
1535
1612
|
//#region src/methods.ts
|
|
1536
1613
|
/**
|
|
1537
1614
|
* 命令。host → iframe(iframe 模式),或消费方 → player-core(inline 模式)。
|
|
1538
1615
|
*
|
|
1539
|
-
*
|
|
1616
|
+
* 权威来源:本 Zod schema;docs/protocol/methods.md 是人类可读说明。
|
|
1540
1617
|
* 每条命令的响应都是 `void`——结果通过事件回来,不通过 response payload。
|
|
1541
1618
|
*/
|
|
1542
1619
|
const CommandSchema = zod.z.discriminatedUnion("method", [
|
|
@@ -1591,9 +1668,9 @@ const CommandSchema = zod.z.discriminatedUnion("method", [
|
|
|
1591
1668
|
* 消费方通常不直接调它 —— 各消费面 watch `locale` prop 后自动下发,
|
|
1592
1669
|
* 改 prop 即可切换(见 ADR-035)。
|
|
1593
1670
|
*
|
|
1594
|
-
* 作用于两处:①播放内核自带控件的文案(
|
|
1595
|
-
* ②player-ui 覆盖层文案(
|
|
1596
|
-
*
|
|
1671
|
+
* 作用于两处:①播放内核自带控件的文案(官方中 / 英或注入的第三语言);
|
|
1672
|
+
* ②player-ui 覆盖层文案(解析官方资源及 `messages`)。
|
|
1673
|
+
* 传字符串可选内置语言;第三语言需传带资源表的对象。
|
|
1597
1674
|
*/
|
|
1598
1675
|
params: zod.z.object({ locale: LocaleConfigSchema })
|
|
1599
1676
|
}),
|
|
@@ -1639,7 +1716,7 @@ const CommandSchema = zod.z.discriminatedUnion("method", [
|
|
|
1639
1716
|
* 唯一来源是 contract-version.json(ADR-094);只在真实协议变化时升级。
|
|
1640
1717
|
* npm 包版本同步不能改变握手与 envelope 的版本。
|
|
1641
1718
|
*/
|
|
1642
|
-
const CONTRACT_VERSION = "
|
|
1719
|
+
const CONTRACT_VERSION = "4.0.0";
|
|
1643
1720
|
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
1644
1721
|
/**
|
|
1645
1722
|
* 解析 semver 字符串。只接受严格的 `major.minor.patch`,
|
|
@@ -1813,77 +1890,249 @@ function errorEnvelope(payload, meta) {
|
|
|
1813
1890
|
};
|
|
1814
1891
|
}
|
|
1815
1892
|
//#endregion
|
|
1816
|
-
//#region src/
|
|
1893
|
+
//#region src/error-action.ts
|
|
1894
|
+
/** 只接受终态、错误码与组件能力都支持的动作;旧生产者缺 action 时不猜测。 */
|
|
1895
|
+
function effectiveErrorAction(input) {
|
|
1896
|
+
const { reason, recoverable, action, errorCode } = input;
|
|
1897
|
+
if (recoverable || !action || action === "none") return "none";
|
|
1898
|
+
if (reason === "autoplay_blocked") return action === "play" && errorCode === "E_AUTOPLAY_BLOCKED" ? "play" : "none";
|
|
1899
|
+
if (reason === "frame_disconnected") return action === "recreate-frame" && input.canRecreateFrame && (errorCode === "E_LOAD_FAILED" || errorCode === "E_HANDSHAKE_TIMEOUT" || errorCode === "E_FRAME_CRASHED") ? "recreate-frame" : "none";
|
|
1900
|
+
if (reason !== "error") return "none";
|
|
1901
|
+
if (action === "replace-source") return errorCode === "E_AUTH_EXPIRED" && input.canResolveSource ? action : "none";
|
|
1902
|
+
if (action === "retry") return errorCode === "E_NETWORK" || errorCode === "E_NETWORK_TIMEOUT" || errorCode === "E_MEDIA_DECODE" || errorCode === "E_MEDIA_ABORTED" ? action : "none";
|
|
1903
|
+
return "none";
|
|
1904
|
+
}
|
|
1905
|
+
/** 有效动作对应的官方文案 key;网络重连与媒体重试保留不同动词。 */
|
|
1906
|
+
function errorActionMessageKey(action, errorCode) {
|
|
1907
|
+
switch (action) {
|
|
1908
|
+
case "play": return "play";
|
|
1909
|
+
case "retry": return errorCode === "E_NETWORK" || errorCode === "E_NETWORK_TIMEOUT" ? "reconnect" : "retry";
|
|
1910
|
+
case "replace-source": return "refreshSource";
|
|
1911
|
+
case "recreate-frame": return "recreatePlayer";
|
|
1912
|
+
default: return null;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
//#endregion
|
|
1916
|
+
//#region src/error-copy.ts
|
|
1817
1917
|
/**
|
|
1818
|
-
*
|
|
1918
|
+
* 错误码 → 面向终端用户的英文兜底文案(#116)。另有简体中文表。
|
|
1819
1919
|
*
|
|
1820
|
-
*
|
|
1821
|
-
* (`apps/embed-app`)都要做这件事,而它俩没有别的公共依赖。此前各写了一份
|
|
1822
|
-
* `readLocale`,两份逐字相同 —— **四种接入方式行为漂移的经典来源**。
|
|
1823
|
-
* 先例:`resolvePreset` 同样是住在 protocol 的纯函数。
|
|
1920
|
+
* ## 为什么要有它——这是对「SDK 不自带文案」的一处例外
|
|
1824
1921
|
*
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1922
|
+
* `locale.ts` 立的规矩是「缺 key 就原样露出 key」,理由是漏翻译要一眼可见。那条规矩对
|
|
1923
|
+
* 普通 UI 文案成立:露出 `retry` 只是难看。**错误文案不一样**:覆盖层原本回退到
|
|
1924
|
+
* `PlayerError.message`,而那个 message 是内核串——实测 403 鉴权失败时,屏幕上显示的是
|
|
1925
|
+
* `内核错误:networkError / levelLoadError`:错误码明明是 `E_AUTH_EXPIRED`、category 是
|
|
1926
|
+
* `auth`,给用户看的字却在说「网络错误」,还把 hls.js 的内部 `ErrorDetails` 抛给了终端用户。
|
|
1927
|
+
* 露 key 只是难看,**露内核串是误导**——用户照着它去查网络,而正确动作是重新鉴权。
|
|
1827
1928
|
*
|
|
1828
|
-
*
|
|
1929
|
+
* ## 语言范围
|
|
1829
1930
|
*
|
|
1830
|
-
*
|
|
1831
|
-
* `I18nProvider` / `useT()` 一起删掉了(player-ui 去 React 那次)。
|
|
1832
|
-
* 今天做这件事的是三个消费面各自的 `resolveText`
|
|
1833
|
-
*(`react/src/overlay-elements.tsx` / `vue/src/overlay-elements.ts` / `embed-app/src/app.ts`),
|
|
1834
|
-
* 实现都是 `messages[key] ?? key`。
|
|
1835
|
-
* 那是刻意的设计:漏翻译时界面上明晃晃出现 `error.network`,
|
|
1836
|
-
* 一眼可见;而静默替换成别的语种只会把问题藏起来。
|
|
1931
|
+
* SDK 内置简体中文和英文,其他语言以及品牌语气由宿主通过 `locale.messages` 提供。
|
|
1837
1932
|
*
|
|
1838
|
-
*
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1933
|
+
* ## 宿主怎么覆盖
|
|
1934
|
+
*
|
|
1935
|
+
* 在 `locale.messages` 里提供 `error.<CODE>`(如 `error.E_AUTH_EXPIRED`)即可,优先级高于
|
|
1936
|
+
* 本表。四个消费包的 README 有完整 key 清单。
|
|
1937
|
+
*
|
|
1938
|
+
* ## 写作口径(改文案时照这三条审)
|
|
1939
|
+
*
|
|
1940
|
+
* 1. **不出现内核术语**(`networkError` / `levelLoadError` / `MediaError` 这类);
|
|
1941
|
+
* 2. **不误导用户去做错的事**——鉴权失效不能说成网络问题;
|
|
1942
|
+
* 3. **给得出下一步**,或至少说明这次能不能重试(与 `ERROR_META.retryable` 一致)。
|
|
1943
|
+
*/
|
|
1944
|
+
const DEFAULT_ERROR_MESSAGES_EN = {
|
|
1945
|
+
E_NETWORK: "Network problem. Check your connection and try again.",
|
|
1946
|
+
E_NETWORK_TIMEOUT: "The connection timed out. Check your network and try again.",
|
|
1947
|
+
E_LOAD_FAILED: "Could not load the video. Check your connection and try again.",
|
|
1948
|
+
E_HANDSHAKE_TIMEOUT: "The player took too long to start. Try again.",
|
|
1949
|
+
E_AUTH_EXPIRED: "Your access to this video has expired. Reload the page to continue.",
|
|
1950
|
+
E_MEDIA_DECODE: "The video could not be decoded. Try again.",
|
|
1951
|
+
E_MEDIA_ABORTED: "Playback was interrupted. Try again.",
|
|
1952
|
+
E_MEDIA_NOT_SUPPORTED: "This video format cannot be played here.",
|
|
1953
|
+
E_MANIFEST_PARSE: "The video could not be read. Try again later.",
|
|
1954
|
+
E_SUBTITLE_LOAD_FAILED: "Subtitles could not be loaded. Playback continues without them.",
|
|
1955
|
+
E_AUTOPLAY_BLOCKED: "Tap play to start the video.",
|
|
1956
|
+
E_ENV_CSP_BLOCKED: "This page blocked the player from loading.",
|
|
1957
|
+
E_METHOD_NOT_SUPPORTED: "This action is not available here.",
|
|
1958
|
+
E_DANMAKU_SEND_FAILED: "Your message could not be sent. Try again.",
|
|
1959
|
+
E_PLAYER_DESTROYED: "The player is no longer available. Reload the page.",
|
|
1960
|
+
E_FRAME_CRASHED: "The player stopped unexpectedly. Reload the page.",
|
|
1961
|
+
E_HANDSHAKE_VERSION_MISMATCH: "The player version does not match. Reload the page.",
|
|
1962
|
+
E_INTERNAL: "Something went wrong in the player. Reload the page.",
|
|
1963
|
+
E_UNKNOWN: "Playback ran into a problem. Try again later."
|
|
1964
|
+
};
|
|
1965
|
+
/** 简体中文默认错误提示;其他语言可通过 `locale.messages` 覆盖。 */
|
|
1966
|
+
const DEFAULT_ERROR_MESSAGES_ZH = {
|
|
1967
|
+
E_NETWORK: "网络连接异常,请检查网络后重试。",
|
|
1968
|
+
E_NETWORK_TIMEOUT: "连接超时,请检查网络后重试。",
|
|
1969
|
+
E_LOAD_FAILED: "视频加载失败,请检查网络后重试。",
|
|
1970
|
+
E_HANDSHAKE_TIMEOUT: "播放器连接超时,请重试。",
|
|
1971
|
+
E_AUTH_EXPIRED: "播放地址已失效,请获取新的播放地址。",
|
|
1972
|
+
E_MEDIA_DECODE: "视频解码失败,请重试播放。",
|
|
1973
|
+
E_MEDIA_ABORTED: "播放已中断,请重试。",
|
|
1974
|
+
E_MEDIA_NOT_SUPPORTED: "当前设备无法播放此视频格式。",
|
|
1975
|
+
E_MANIFEST_PARSE: "视频内容无法读取,请稍后再试或更换播放源。",
|
|
1976
|
+
E_SUBTITLE_LOAD_FAILED: "字幕加载失败,视频仍可继续播放。",
|
|
1977
|
+
E_AUTOPLAY_BLOCKED: "请点击播放以继续。",
|
|
1978
|
+
E_ENV_CSP_BLOCKED: "当前页面阻止了播放器加载。",
|
|
1979
|
+
E_METHOD_NOT_SUPPORTED: "当前无法执行此操作。",
|
|
1980
|
+
E_DANMAKU_SEND_FAILED: "弹幕发送失败,请重试。",
|
|
1981
|
+
E_PLAYER_DESTROYED: "播放器已关闭,请重新打开。",
|
|
1982
|
+
E_FRAME_CRASHED: "播放器连接已断开,请重新打开播放器。",
|
|
1983
|
+
E_HANDSHAKE_VERSION_MISMATCH: "播放器版本不匹配,请刷新页面。",
|
|
1984
|
+
E_INTERNAL: "播放器发生异常,请刷新页面。",
|
|
1985
|
+
E_UNKNOWN: "播放遇到问题,请稍后再试。"
|
|
1986
|
+
};
|
|
1987
|
+
/**
|
|
1988
|
+
* 解析错误文案:宿主翻译 → 当前语言的中 / 英兜底。
|
|
1989
|
+
*
|
|
1990
|
+
* **不再回退到 `PlayerError.message`** —— 那是内核串,给用户看会误导(见本文件头注释)。
|
|
1991
|
+
* 排查需要的原始信息仍在 `PlayerError.cause` 里(`errorType` / `errorDetails` / `status`)。
|
|
1992
|
+
*
|
|
1993
|
+
* `onFallback` 供消费面在开发构建里提示宿主「这个码你没配翻译」,保住 `locale.ts`
|
|
1994
|
+
* 「漏翻译要一眼可见」的原意,又不让线上用户吃这个亏。
|
|
1846
1995
|
*/
|
|
1996
|
+
function resolveErrorMessage(messages, code, onFallback, locale = "zh-CN") {
|
|
1997
|
+
const key = `error.${code}`;
|
|
1998
|
+
const translated = messages[key];
|
|
1999
|
+
if (typeof translated === "string" && translated.length > 0) return translated;
|
|
2000
|
+
onFallback?.(key);
|
|
2001
|
+
const defaults = /^en(?:[-_]|$)/i.test(locale) ? DEFAULT_ERROR_MESSAGES_EN : DEFAULT_ERROR_MESSAGES_ZH;
|
|
2002
|
+
return defaults[code] ?? defaults[fallbackCodeFor(code)];
|
|
2003
|
+
}
|
|
2004
|
+
/** 未知码按 `E_UNKNOWN` 兜底;已知码但没写文案(理论上不会发生)按同类兜底 */
|
|
2005
|
+
function fallbackCodeFor(code) {
|
|
2006
|
+
const meta = ERROR_META[code];
|
|
2007
|
+
if (meta === void 0) return "E_UNKNOWN";
|
|
2008
|
+
return meta.category === "network" ? "E_NETWORK" : "E_UNKNOWN";
|
|
2009
|
+
}
|
|
2010
|
+
//#endregion
|
|
2011
|
+
//#region src/official-messages.ts
|
|
2012
|
+
const enOverlay = {
|
|
2013
|
+
loading: "Loading…",
|
|
2014
|
+
"loading.initializing": "Starting player…",
|
|
2015
|
+
"loading.buffering": "Buffering…",
|
|
2016
|
+
"loading.stalled": "Restoring playback…",
|
|
2017
|
+
"loading.reconnecting": "Reconnecting…",
|
|
2018
|
+
"loading.source_switching": "Switching video source…",
|
|
2019
|
+
"loading.frame_disconnected": "Connecting to player…",
|
|
2020
|
+
play: "Play video",
|
|
2021
|
+
retry: "Retry playback",
|
|
2022
|
+
reconnect: "Reconnect",
|
|
2023
|
+
refreshSource: "Get a new playback URL",
|
|
2024
|
+
recreatePlayer: "Reconnect player"
|
|
2025
|
+
};
|
|
2026
|
+
const zhOverlay = {
|
|
2027
|
+
loading: "加载中…",
|
|
2028
|
+
"loading.initializing": "正在初始化播放器…",
|
|
2029
|
+
"loading.buffering": "正在缓冲…",
|
|
2030
|
+
"loading.stalled": "正在恢复播放…",
|
|
2031
|
+
"loading.reconnecting": "正在重新连接…",
|
|
2032
|
+
"loading.source_switching": "正在换源…",
|
|
2033
|
+
"loading.frame_disconnected": "正在连接播放器…",
|
|
2034
|
+
play: "播放视频",
|
|
2035
|
+
retry: "重试播放",
|
|
2036
|
+
reconnect: "重新连接",
|
|
2037
|
+
refreshSource: "重新获取播放地址",
|
|
2038
|
+
recreatePlayer: "重新连接播放器"
|
|
2039
|
+
};
|
|
2040
|
+
const enControls = {
|
|
2041
|
+
"controls.play": "Play",
|
|
2042
|
+
"controls.pause": "Pause",
|
|
2043
|
+
"controls.replay": "Replay",
|
|
2044
|
+
"controls.fullscreen.enter": "Fullscreen",
|
|
2045
|
+
"controls.fullscreen.exit": "Exit fullscreen",
|
|
2046
|
+
"controls.pageFullscreen.enter": "CSS fullscreen",
|
|
2047
|
+
"controls.pageFullscreen.exit": "Exit CSS fullscreen",
|
|
2048
|
+
"controls.pip": "Picture in picture",
|
|
2049
|
+
"controls.subtitle": "Captions",
|
|
2050
|
+
"controls.subtitle.off": "Off",
|
|
2051
|
+
"controls.live": "Live",
|
|
2052
|
+
"controls.rotate": "Rotate"
|
|
2053
|
+
};
|
|
2054
|
+
const zhControls = {
|
|
2055
|
+
"controls.play": "播放",
|
|
2056
|
+
"controls.pause": "暂停",
|
|
2057
|
+
"controls.replay": "重播",
|
|
2058
|
+
"controls.fullscreen.enter": "进入全屏",
|
|
2059
|
+
"controls.fullscreen.exit": "退出全屏",
|
|
2060
|
+
"controls.pageFullscreen.enter": "进入样式全屏",
|
|
2061
|
+
"controls.pageFullscreen.exit": "退出样式全屏",
|
|
2062
|
+
"controls.pip": "画中画",
|
|
2063
|
+
"controls.subtitle": "字幕",
|
|
2064
|
+
"controls.subtitle.off": "关闭",
|
|
2065
|
+
"controls.live": "直播",
|
|
2066
|
+
"controls.rotate": "旋转"
|
|
2067
|
+
};
|
|
2068
|
+
const errorEntries = (table) => Object.fromEntries(Object.entries(table).map(([code, message]) => [`error.${code}`, message]));
|
|
2069
|
+
/** 播放器内置的完整简体中文文案,也是官方资源 key 清单的来源。 */
|
|
2070
|
+
const zhCN = {
|
|
2071
|
+
...zhOverlay,
|
|
2072
|
+
...errorEntries(DEFAULT_ERROR_MESSAGES_ZH),
|
|
2073
|
+
...zhControls
|
|
2074
|
+
};
|
|
2075
|
+
/** 播放器内置的完整英文文案。 */
|
|
2076
|
+
const enUS = {
|
|
2077
|
+
...enOverlay,
|
|
2078
|
+
...errorEntries(DEFAULT_ERROR_MESSAGES_EN),
|
|
2079
|
+
...enControls
|
|
2080
|
+
};
|
|
2081
|
+
/** 官方资源必须覆盖的播放器自有文案 key。 */
|
|
2082
|
+
const PLAYER_MESSAGE_KEYS = Object.keys(zhCN);
|
|
2083
|
+
/** 只归一官方标签及其别名;未知地区变体保留原样。 */
|
|
2084
|
+
function normalizeLocaleTag(tag) {
|
|
2085
|
+
const normalized = tag.replaceAll("_", "-").toLowerCase();
|
|
2086
|
+
if (normalized === "zh" || normalized === "zh-cn") return "zh-CN";
|
|
2087
|
+
if (normalized === "en" || normalized === "en-us") return "en-US";
|
|
2088
|
+
if (normalized === "vi" || normalized === "vi-vn") return "vi-VN";
|
|
2089
|
+
return tag;
|
|
2090
|
+
}
|
|
2091
|
+
/** protocol 不依赖按需语言包;越南语由宿主注入。 */
|
|
2092
|
+
function builtInMessages(tag) {
|
|
2093
|
+
const normalized = normalizeLocaleTag(tag);
|
|
2094
|
+
if (normalized === "zh-CN") return zhCN;
|
|
2095
|
+
if (normalized === "en-US") return enUS;
|
|
2096
|
+
}
|
|
2097
|
+
//#endregion
|
|
2098
|
+
//#region src/locale.ts
|
|
2099
|
+
/** 覆盖层文案:有效资源表 → 旧版通用 loading → 官方中文。 */
|
|
2100
|
+
function resolveOverlayMessage(messages, key, locale = "zh-CN") {
|
|
2101
|
+
if (messages[key]) return messages[key];
|
|
2102
|
+
if (key.startsWith("loading.") && messages.loading) return messages.loading;
|
|
2103
|
+
return builtInMessages(locale)?.[key] ?? zhCN[key] ?? key;
|
|
2104
|
+
}
|
|
2105
|
+
/** 按当前语言、显式回退链、官方中文的优先级逐 key 合成实例快照。 */
|
|
1847
2106
|
function resolveLocaleMessages(locale) {
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2107
|
+
const tag = normalizeLocaleTag((typeof locale === "string" ? locale : locale?.locale) || "zh-CN");
|
|
2108
|
+
const fallbackLocales = typeof locale === "object" ? locale.fallbackLocales ?? [] : [];
|
|
2109
|
+
const custom = typeof locale === "object" ? locale.messages : void 0;
|
|
2110
|
+
const tables = /* @__PURE__ */ new Map();
|
|
2111
|
+
for (const [key, value] of Object.entries(custom ?? {})) tables.set(normalizeLocaleTag(key), value);
|
|
2112
|
+
const messages = Object.create(null);
|
|
2113
|
+
const chain = [
|
|
2114
|
+
"zh-CN",
|
|
2115
|
+
...fallbackLocales.map(normalizeLocaleTag).reverse(),
|
|
2116
|
+
tag
|
|
2117
|
+
].filter((value, index, entries) => entries.lastIndexOf(value) === index);
|
|
2118
|
+
for (const current of chain) {
|
|
2119
|
+
Object.assign(messages, builtInMessages(current));
|
|
2120
|
+
const entries = tables.get(current);
|
|
2121
|
+
if (!entries) continue;
|
|
2122
|
+
const genericLoading = entries.loading;
|
|
2123
|
+
if (genericLoading) {
|
|
2124
|
+
for (const key of Object.keys(zhCN)) if (key.startsWith("loading.") && !entries[key]) messages[key] = genericLoading;
|
|
2125
|
+
}
|
|
2126
|
+
for (const [key, value] of Object.entries(entries)) if (value) messages[key] = value;
|
|
2127
|
+
}
|
|
1865
2128
|
return {
|
|
1866
|
-
locale:
|
|
2129
|
+
locale: tag,
|
|
1867
2130
|
messages
|
|
1868
2131
|
};
|
|
1869
2132
|
}
|
|
1870
|
-
//#endregion
|
|
1871
|
-
//#region src/presets.ts
|
|
1872
|
-
/**
|
|
1873
|
-
* 场景预设表。权威来源:ARCHITECTURE.md § 8.7.3。
|
|
1874
|
-
*
|
|
1875
|
-
* **只保留一个预设 `homepage-preview`(ADR-032)**:预设的价值在于打包一组"不平凡"的字段组合,
|
|
1876
|
-
* 而首页/直播预览卡片正是这样的组合(静音循环自动播 + 无控件 + 不响应点击)——高频复用、
|
|
1877
|
-
* 手写六个字段易错。其余场景(如房内"自动播 + 静音")字段太少,直接写 prop 即可,不配拥有预设。
|
|
1878
|
-
* 曾经的 `sea-mobile` / `sea-tv` / `desktop` / `internal-admin` 已删(首发前收敛,见 ADR-032)。
|
|
1879
|
-
*
|
|
1880
|
-
* 注意:§ 8.7.3 里预览预设还写了 `danmaku` / `wakeLock` / `visibility` 字段,但**刻意不进 preset 默认**
|
|
1881
|
-
* ——弹幕是直播按需能力,由消费方显式开(M1.1 D1 决策:档 C);`wakeLock` / `visibility` 是 P0/P1
|
|
1882
|
-
* 插件的内部行为,不走契约配置。故此表只保留通用播放字段。
|
|
1883
|
-
*/
|
|
1884
2133
|
const PRESETS = {
|
|
1885
|
-
/**
|
|
1886
|
-
"
|
|
2134
|
+
/** 环境式预览:静音循环自动播,无控件,不响应点击(透传给外层容器) */
|
|
2135
|
+
"ambient-preview": {
|
|
1887
2136
|
autoplay: true,
|
|
1888
2137
|
muted: true,
|
|
1889
2138
|
loop: true,
|
|
@@ -1900,7 +2149,7 @@ const PRESETS = {
|
|
|
1900
2149
|
* @param config 消费方传入的配置(含 source)
|
|
1901
2150
|
*
|
|
1902
2151
|
* @example
|
|
1903
|
-
* resolvePreset('
|
|
2152
|
+
* resolvePreset('ambient-preview', { source: 'a.m3u8', autoplay: false })
|
|
1904
2153
|
* // → autoplay: false(显式值赢),muted: true(来自预设)
|
|
1905
2154
|
*/
|
|
1906
2155
|
function resolvePreset(preset, config) {
|
|
@@ -1910,16 +2159,76 @@ function resolvePreset(preset, config) {
|
|
|
1910
2159
|
return merged;
|
|
1911
2160
|
}
|
|
1912
2161
|
//#endregion
|
|
2162
|
+
//#region src/provider-config.ts
|
|
2163
|
+
function mergeFrameDefaults(parent = {}, child = {}) {
|
|
2164
|
+
const merged = { ...parent };
|
|
2165
|
+
for (const [key, value] of Object.entries(child)) if (value !== void 0) merged[key] = value;
|
|
2166
|
+
return merged;
|
|
2167
|
+
}
|
|
2168
|
+
const OBJECT_FIELDS = ["errorButtonColors", "controlVisibility"];
|
|
2169
|
+
/** 父子 Provider 合成;只有两种配置对象按子字段继承。 */
|
|
2170
|
+
function mergePlayerDefaults(parent = {}, child = {}) {
|
|
2171
|
+
const merged = { ...parent };
|
|
2172
|
+
for (const [key, value] of Object.entries(child)) {
|
|
2173
|
+
if (value === void 0) continue;
|
|
2174
|
+
if (OBJECT_FIELDS.includes(key)) merged[key] = {
|
|
2175
|
+
...parent[key],
|
|
2176
|
+
...value
|
|
2177
|
+
};
|
|
2178
|
+
else merged[key] = value;
|
|
2179
|
+
}
|
|
2180
|
+
return merged;
|
|
2181
|
+
}
|
|
2182
|
+
/** 实例显式值 > 实例 preset > Provider;保留 null 供组件隐藏图片。 */
|
|
2183
|
+
function applyPlayerDefaults(instance, defaults = {}) {
|
|
2184
|
+
const preset = instance.preset ? PRESETS[instance.preset] : void 0;
|
|
2185
|
+
const result = { ...defaults };
|
|
2186
|
+
for (const [key, value] of Object.entries(preset ?? {})) if (value !== void 0) result[key] = value;
|
|
2187
|
+
for (const [key, value] of Object.entries(instance)) {
|
|
2188
|
+
if (value === void 0) continue;
|
|
2189
|
+
if (OBJECT_FIELDS.includes(key)) result[key] = {
|
|
2190
|
+
...defaults[key],
|
|
2191
|
+
...value
|
|
2192
|
+
};
|
|
2193
|
+
else result[key] = value;
|
|
2194
|
+
}
|
|
2195
|
+
return result;
|
|
2196
|
+
}
|
|
2197
|
+
/** 资源按语言和 key 合并,避免内层只覆写一条时丢掉外层其余翻译。 */
|
|
2198
|
+
function mergeMessageResources(parent = {}, child = {}) {
|
|
2199
|
+
const merged = Object.create(null);
|
|
2200
|
+
for (const [tag, messages] of [...Object.entries(parent), ...Object.entries(child)]) {
|
|
2201
|
+
const normalized = normalizeLocaleTag(tag);
|
|
2202
|
+
merged[normalized] = {
|
|
2203
|
+
...merged[normalized],
|
|
2204
|
+
...messages
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
return merged;
|
|
2208
|
+
}
|
|
2209
|
+
/** 按 #125 的优先级把 Provider 与单实例资源交给现有 locale 解析器。 */
|
|
2210
|
+
function applyProviderLocale(instance, providerLocale, resources = {}, overrides = {}, fallbackLocales = []) {
|
|
2211
|
+
const instanceConfig = typeof instance === "string" ? { locale: instance } : instance;
|
|
2212
|
+
const messages = mergeMessageResources(mergeMessageResources(resources, overrides), instanceConfig?.messages);
|
|
2213
|
+
return {
|
|
2214
|
+
locale: instanceConfig?.locale ?? providerLocale,
|
|
2215
|
+
fallbackLocales: instanceConfig?.fallbackLocales ?? fallbackLocales,
|
|
2216
|
+
messages
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
//#endregion
|
|
1913
2220
|
exports.CONTRACT_VERSION = CONTRACT_VERSION;
|
|
1914
2221
|
exports.CommandEnvelopeSchema = CommandEnvelopeSchema;
|
|
1915
2222
|
exports.CommandSchema = CommandSchema;
|
|
1916
2223
|
exports.ControlVisibilityConfigSchema = ControlVisibilityConfigSchema;
|
|
2224
|
+
exports.DEFAULT_ERROR_MESSAGES_EN = DEFAULT_ERROR_MESSAGES_EN;
|
|
2225
|
+
exports.DEFAULT_ERROR_MESSAGES_ZH = DEFAULT_ERROR_MESSAGES_ZH;
|
|
1917
2226
|
exports.DanmakuConfigSchema = DanmakuConfigSchema;
|
|
1918
2227
|
exports.DanmakuItemSchema = DanmakuItemSchema;
|
|
1919
|
-
exports.DeliveredPlayerEventSchema = DeliveredPlayerEventSchema;
|
|
1920
2228
|
exports.ERROR_META = ERROR_META;
|
|
1921
2229
|
exports.EnvelopeSchema = EnvelopeSchema;
|
|
1922
2230
|
exports.EnvelopeTypeSchema = EnvelopeTypeSchema;
|
|
2231
|
+
exports.ErrorButtonColorsSchema = ErrorButtonColorsSchema;
|
|
1923
2232
|
exports.ErrorCategorySchema = ErrorCategorySchema;
|
|
1924
2233
|
exports.ErrorCauseSchema = ErrorCauseSchema;
|
|
1925
2234
|
exports.ErrorCodeSchema = ErrorCodeSchema;
|
|
@@ -1934,6 +2243,7 @@ exports.MediaSourceSchema = MediaSourceSchema;
|
|
|
1934
2243
|
exports.MediaTypeSchema = MediaTypeSchema;
|
|
1935
2244
|
exports.MultiSourceObjectSchema = MultiSourceObjectSchema;
|
|
1936
2245
|
exports.PLAYER_DESTROYED_MESSAGE = PLAYER_DESTROYED_MESSAGE;
|
|
2246
|
+
exports.PLAYER_MESSAGE_KEYS = PLAYER_MESSAGE_KEYS;
|
|
1937
2247
|
exports.PRESETS = PRESETS;
|
|
1938
2248
|
exports.PageFullscreenControlSchema = PageFullscreenControlSchema;
|
|
1939
2249
|
exports.PageFullscreenMessageSchema = PageFullscreenMessageSchema;
|
|
@@ -1945,6 +2255,7 @@ exports.PlaybackKernelSchema = PlaybackKernelSchema;
|
|
|
1945
2255
|
exports.PlaybackRuntimeSchema = PlaybackRuntimeSchema;
|
|
1946
2256
|
exports.PlayerConfigSchema = PlayerConfigSchema;
|
|
1947
2257
|
exports.PlayerErrorSchema = PlayerErrorSchema;
|
|
2258
|
+
exports.PlayerEventDeliverySchema = PlayerEventDeliverySchema;
|
|
1948
2259
|
exports.PlayerEventSchema = PlayerEventSchema;
|
|
1949
2260
|
exports.PosterConfigSchema = PosterConfigSchema;
|
|
1950
2261
|
exports.PresetNameSchema = PresetNameSchema;
|
|
@@ -1955,25 +2266,38 @@ exports.SingleSourceObjectSchema = SingleSourceObjectSchema;
|
|
|
1955
2266
|
exports.SourceEntrySchema = SourceEntrySchema;
|
|
1956
2267
|
exports.SourceEntryTypeSchema = SourceEntryTypeSchema;
|
|
1957
2268
|
exports.SourceRoutePayloadSchema = SourceRoutePayloadSchema;
|
|
2269
|
+
exports.SourceSwitchPayloadSchema = SourceSwitchPayloadSchema;
|
|
1958
2270
|
exports.StreamKindSchema = StreamKindSchema;
|
|
1959
2271
|
exports.SubtitleTrackInfoSchema = SubtitleTrackInfoSchema;
|
|
1960
2272
|
exports.SubtitleTrackSchema = SubtitleTrackSchema;
|
|
1961
2273
|
exports.USER_ACTION_ALLOWLIST = USER_ACTION_ALLOWLIST;
|
|
1962
2274
|
exports.UserActionPayloadSchema = UserActionPayloadSchema;
|
|
1963
2275
|
exports.WarningCodeSchema = WarningCodeSchema;
|
|
2276
|
+
exports.applyPlayerDefaults = applyPlayerDefaults;
|
|
2277
|
+
exports.applyProviderLocale = applyProviderLocale;
|
|
1964
2278
|
exports.commandEnvelope = commandEnvelope;
|
|
1965
2279
|
exports.createEnvelopeSchema = createEnvelopeSchema;
|
|
2280
|
+
exports.effectiveErrorAction = effectiveErrorAction;
|
|
2281
|
+
exports.enUS = enUS;
|
|
2282
|
+
exports.errorActionMessageKey = errorActionMessageKey;
|
|
1966
2283
|
exports.errorEnvelope = errorEnvelope;
|
|
1967
2284
|
exports.eventEnvelope = eventEnvelope;
|
|
1968
2285
|
exports.isContractCompatible = isContractCompatible;
|
|
1969
2286
|
exports.makeEventRejectedWarning = makeEventRejectedWarning;
|
|
1970
2287
|
exports.makePlayerError = makePlayerError;
|
|
2288
|
+
exports.mergeFrameDefaults = mergeFrameDefaults;
|
|
2289
|
+
exports.mergeMessageResources = mergeMessageResources;
|
|
2290
|
+
exports.mergePlayerDefaults = mergePlayerDefaults;
|
|
1971
2291
|
exports.normalizeFrameFreeze = normalizeFrameFreeze;
|
|
2292
|
+
exports.normalizeLocaleTag = normalizeLocaleTag;
|
|
1972
2293
|
exports.normalizeUserAction = normalizeUserAction;
|
|
1973
2294
|
exports.parseVersion = parseVersion;
|
|
1974
2295
|
exports.redactSourceUrl = redactSourceUrl;
|
|
2296
|
+
exports.resolveErrorMessage = resolveErrorMessage;
|
|
1975
2297
|
exports.resolveLocaleMessages = resolveLocaleMessages;
|
|
2298
|
+
exports.resolveOverlayMessage = resolveOverlayMessage;
|
|
1976
2299
|
exports.resolvePreset = resolvePreset;
|
|
1977
2300
|
exports.responseEnvelope = responseEnvelope;
|
|
1978
2301
|
exports.serializeCause = serializeCause;
|
|
1979
2302
|
exports.toErrorEvent = toErrorEvent;
|
|
2303
|
+
exports.zhCN = zhCN;
|