@video-lab/player-core 4.1.1 → 4.1.3
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 +2 -0
- package/dist/index.cjs +36 -5
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +36 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ window.addEventListener('pagehide', () => player.destroy(), { once: true })
|
|
|
103
103
|
|
|
104
104
|
**换内核会明确报错。** hls.js / flv.js 的内核插件在构造时注册,运行时换不了。`load()` 一个需要不同内核的源时抛 `E_METHOD_NOT_SUPPORTED`,而不是悄悄播不出来。消费方需要销毁 player 用新 source 重建。
|
|
105
105
|
|
|
106
|
+
**新源加载失败只经 `error` 事件报告一次。** `load()` 在这次换源有结论时 resolve:新源起播、源故障已通过 `error` 报出、被更新的 `load()` 取代或播放器销毁。它只在换源前的校验失败(非法 source、换内核、改构造期配置、选不出可播放源)时 reject(ADR-136);reject 前会先暂停旧源,避免它在错误层下继续播放(ADR-137)。
|
|
107
|
+
|
|
106
108
|
**直播的 `duration` 是 0,不是 Infinity。** Infinity 过不了 JSON 序列化(变成 null),而事件要跨 iframe 传,所以在源头就归一成 0。
|
|
107
109
|
|
|
108
110
|
## 环境探测与测试
|
package/dist/index.cjs
CHANGED
|
@@ -3175,8 +3175,15 @@ function createPlayer(options) {
|
|
|
3175
3175
|
let recovery = null;
|
|
3176
3176
|
let eventStreamReady = false;
|
|
3177
3177
|
const deferredEvents = [];
|
|
3178
|
+
/**
|
|
3179
|
+
* 挂起中的 `load()` 的结束口(ADR-136)。换源后的源故障只经 `error` 事件报告一次;
|
|
3180
|
+
* 由它结束 `load()`,不再等 xgplayer `switchURL` —— 那个 Promise 只认 xgplayer 自己的
|
|
3181
|
+
* `error` / `canplay`,而 HLS 致命错误走 `HLS_ERROR`,不会触发它(#145)。
|
|
3182
|
+
*/
|
|
3183
|
+
let settlePendingLoad = null;
|
|
3184
|
+
const isSourceFault = (event, eventSession) => event.event === "error" && !destroyed && eventSession === sessionId && event.payload.code !== "E_SUBTITLE_LOAD_FAILED" && event.payload.code !== "E_DANMAKU_SEND_FAILED" && event.payload.code !== "E_METHOD_NOT_SUPPORTED";
|
|
3178
3185
|
const reportRecoveryFault = (event, eventSession) => {
|
|
3179
|
-
if (event.event === "error" &&
|
|
3186
|
+
if (event.event === "error" && isSourceFault(event, eventSession)) recovery?.fault(event.payload, eventSession);
|
|
3180
3187
|
};
|
|
3181
3188
|
const emit = (event) => {
|
|
3182
3189
|
if (destroyed) return;
|
|
@@ -3187,6 +3194,7 @@ function createPlayer(options) {
|
|
|
3187
3194
|
}
|
|
3188
3195
|
const eventSession = sessionId;
|
|
3189
3196
|
notify(delivered);
|
|
3197
|
+
if (isSourceFault(event, eventSession)) settlePendingLoad?.();
|
|
3190
3198
|
reportRecoveryFault(event, eventSession);
|
|
3191
3199
|
};
|
|
3192
3200
|
let playerRef = null;
|
|
@@ -4055,8 +4063,15 @@ function createPlayer(options) {
|
|
|
4055
4063
|
},
|
|
4056
4064
|
async load(source) {
|
|
4057
4065
|
if (destroyed) return;
|
|
4066
|
+
const pauseBeforeReject = () => {
|
|
4067
|
+
setPlayingIntent(false);
|
|
4068
|
+
player.pause();
|
|
4069
|
+
};
|
|
4058
4070
|
const parsedSource = _video_lab_protocol.MediaSourceSchema.safeParse(source);
|
|
4059
|
-
if (!parsedSource.success)
|
|
4071
|
+
if (!parsedSource.success) {
|
|
4072
|
+
pauseBeforeReject();
|
|
4073
|
+
throwPlayerError("E_METHOD_NOT_SUPPORTED", `非法换源:${parsedSource.error.issues.map((i) => `${i.path.join(".") || "(根)"} ${i.message}`).join("; ")}`);
|
|
4074
|
+
}
|
|
4060
4075
|
const nextNormalized = normalizeSource(source);
|
|
4061
4076
|
const nextSessionId = newSessionId();
|
|
4062
4077
|
let nextRouted;
|
|
@@ -4067,11 +4082,18 @@ function createPlayer(options) {
|
|
|
4067
4082
|
event: "sourceroute",
|
|
4068
4083
|
payload: unsupportedRoutePayload(nextSessionId, nextNormalized, env)
|
|
4069
4084
|
});
|
|
4085
|
+
pauseBeforeReject();
|
|
4070
4086
|
throw error;
|
|
4071
4087
|
}
|
|
4072
|
-
if (nextRouted.kernel !== routed.kernel)
|
|
4088
|
+
if (nextRouted.kernel !== routed.kernel) {
|
|
4089
|
+
pauseBeforeReject();
|
|
4090
|
+
throwPlayerError("E_METHOD_NOT_SUPPORTED", `换源需要从 ${routed.kernel} 内核切到 ${nextRouted.kernel},运行时无法切换内核。请销毁当前 player 并用新的 source 重建。`);
|
|
4091
|
+
}
|
|
4073
4092
|
const constructionChanges = sourceConstructionChanges(normalized, nextNormalized);
|
|
4074
|
-
if (constructionChanges.length > 0)
|
|
4093
|
+
if (constructionChanges.length > 0) {
|
|
4094
|
+
pauseBeforeReject();
|
|
4095
|
+
throwPlayerError("E_METHOD_NOT_SUPPORTED", `换源不能改变构造期 source 配置(${constructionChanges.join("、")})。播放器仍使用创建时的直播、HLS 与字幕插件配置。请销毁当前 player 并用新的 source 重建。`);
|
|
4096
|
+
}
|
|
4075
4097
|
const revision = ++loadRevision;
|
|
4076
4098
|
naturalProof?.abort();
|
|
4077
4099
|
recovery?.setSession(nextSessionId);
|
|
@@ -4096,7 +4118,15 @@ function createPlayer(options) {
|
|
|
4096
4118
|
if (destroyed || revision !== loadRevision) return;
|
|
4097
4119
|
emitContextChange();
|
|
4098
4120
|
if (destroyed || revision !== loadRevision) return;
|
|
4099
|
-
|
|
4121
|
+
settlePendingLoad?.();
|
|
4122
|
+
await new Promise((resolve) => {
|
|
4123
|
+
const settle = () => {
|
|
4124
|
+
if (settlePendingLoad === settle) settlePendingLoad = null;
|
|
4125
|
+
resolve();
|
|
4126
|
+
};
|
|
4127
|
+
settlePendingLoad = settle;
|
|
4128
|
+
Promise.resolve(player.switchURL(nextRouted.chosen.url)).then(settle, settle);
|
|
4129
|
+
});
|
|
4100
4130
|
if (destroyed || revision !== loadRevision) return;
|
|
4101
4131
|
getMediaSession(player)?.setMetadata(nextNormalized.metadata);
|
|
4102
4132
|
},
|
|
@@ -4120,6 +4150,7 @@ function createPlayer(options) {
|
|
|
4120
4150
|
*/
|
|
4121
4151
|
destroy() {
|
|
4122
4152
|
if (destroyed) return;
|
|
4153
|
+
settlePendingLoad?.();
|
|
4123
4154
|
recovery?.destroy();
|
|
4124
4155
|
recoveryCleanup?.();
|
|
4125
4156
|
destroyed = true;
|
package/dist/index.d.cts
CHANGED
|
@@ -159,6 +159,11 @@ interface PlayerCoreHandle {
|
|
|
159
159
|
* 安全热切媒体路由。改变内核、`live`、`hls.lowLatencyMode` 或字幕配置会抛
|
|
160
160
|
* `E_METHOD_NOT_SUPPORTED`,因为它们在播放器构造期确定;宿主须销毁后以新 source 重建。
|
|
161
161
|
* 组合层的常规换源入口是 `source` prop,直接调用只适合 player-core / 高级句柄场景。
|
|
162
|
+
*
|
|
163
|
+
* 返回的 Promise 在这次换源有结论时 resolve(ADR-136):新源起播、当前会话的源故障已经通过
|
|
164
|
+
* `error` 事件报出、被更新的 `load()` 取代或播放器销毁。resolve 不代表新源可播放;
|
|
165
|
+
* 新源加载失败只经 `error` 事件报告一次,不会让 `load()` reject。只有上面的校验失败才 reject,
|
|
166
|
+
* 且 reject 前会暂停旧源(ADR-137),避免它在错误层下继续播放。
|
|
162
167
|
*/
|
|
163
168
|
load(source: MediaSource): Promise<void>;
|
|
164
169
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -159,6 +159,11 @@ interface PlayerCoreHandle {
|
|
|
159
159
|
* 安全热切媒体路由。改变内核、`live`、`hls.lowLatencyMode` 或字幕配置会抛
|
|
160
160
|
* `E_METHOD_NOT_SUPPORTED`,因为它们在播放器构造期确定;宿主须销毁后以新 source 重建。
|
|
161
161
|
* 组合层的常规换源入口是 `source` prop,直接调用只适合 player-core / 高级句柄场景。
|
|
162
|
+
*
|
|
163
|
+
* 返回的 Promise 在这次换源有结论时 resolve(ADR-136):新源起播、当前会话的源故障已经通过
|
|
164
|
+
* `error` 事件报出、被更新的 `load()` 取代或播放器销毁。resolve 不代表新源可播放;
|
|
165
|
+
* 新源加载失败只经 `error` 事件报告一次,不会让 `load()` reject。只有上面的校验失败才 reject,
|
|
166
|
+
* 且 reject 前会暂停旧源(ADR-137),避免它在错误层下继续播放。
|
|
162
167
|
*/
|
|
163
168
|
load(source: MediaSource): Promise<void>;
|
|
164
169
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -3149,8 +3149,15 @@ function createPlayer(options) {
|
|
|
3149
3149
|
let recovery = null;
|
|
3150
3150
|
let eventStreamReady = false;
|
|
3151
3151
|
const deferredEvents = [];
|
|
3152
|
+
/**
|
|
3153
|
+
* 挂起中的 `load()` 的结束口(ADR-136)。换源后的源故障只经 `error` 事件报告一次;
|
|
3154
|
+
* 由它结束 `load()`,不再等 xgplayer `switchURL` —— 那个 Promise 只认 xgplayer 自己的
|
|
3155
|
+
* `error` / `canplay`,而 HLS 致命错误走 `HLS_ERROR`,不会触发它(#145)。
|
|
3156
|
+
*/
|
|
3157
|
+
let settlePendingLoad = null;
|
|
3158
|
+
const isSourceFault = (event, eventSession) => event.event === "error" && !destroyed && eventSession === sessionId && event.payload.code !== "E_SUBTITLE_LOAD_FAILED" && event.payload.code !== "E_DANMAKU_SEND_FAILED" && event.payload.code !== "E_METHOD_NOT_SUPPORTED";
|
|
3152
3159
|
const reportRecoveryFault = (event, eventSession) => {
|
|
3153
|
-
if (event.event === "error" &&
|
|
3160
|
+
if (event.event === "error" && isSourceFault(event, eventSession)) recovery?.fault(event.payload, eventSession);
|
|
3154
3161
|
};
|
|
3155
3162
|
const emit = (event) => {
|
|
3156
3163
|
if (destroyed) return;
|
|
@@ -3161,6 +3168,7 @@ function createPlayer(options) {
|
|
|
3161
3168
|
}
|
|
3162
3169
|
const eventSession = sessionId;
|
|
3163
3170
|
notify(delivered);
|
|
3171
|
+
if (isSourceFault(event, eventSession)) settlePendingLoad?.();
|
|
3164
3172
|
reportRecoveryFault(event, eventSession);
|
|
3165
3173
|
};
|
|
3166
3174
|
let playerRef = null;
|
|
@@ -4029,8 +4037,15 @@ function createPlayer(options) {
|
|
|
4029
4037
|
},
|
|
4030
4038
|
async load(source) {
|
|
4031
4039
|
if (destroyed) return;
|
|
4040
|
+
const pauseBeforeReject = () => {
|
|
4041
|
+
setPlayingIntent(false);
|
|
4042
|
+
player.pause();
|
|
4043
|
+
};
|
|
4032
4044
|
const parsedSource = MediaSourceSchema.safeParse(source);
|
|
4033
|
-
if (!parsedSource.success)
|
|
4045
|
+
if (!parsedSource.success) {
|
|
4046
|
+
pauseBeforeReject();
|
|
4047
|
+
throwPlayerError("E_METHOD_NOT_SUPPORTED", `非法换源:${parsedSource.error.issues.map((i) => `${i.path.join(".") || "(根)"} ${i.message}`).join("; ")}`);
|
|
4048
|
+
}
|
|
4034
4049
|
const nextNormalized = normalizeSource(source);
|
|
4035
4050
|
const nextSessionId = newSessionId();
|
|
4036
4051
|
let nextRouted;
|
|
@@ -4041,11 +4056,18 @@ function createPlayer(options) {
|
|
|
4041
4056
|
event: "sourceroute",
|
|
4042
4057
|
payload: unsupportedRoutePayload(nextSessionId, nextNormalized, env)
|
|
4043
4058
|
});
|
|
4059
|
+
pauseBeforeReject();
|
|
4044
4060
|
throw error;
|
|
4045
4061
|
}
|
|
4046
|
-
if (nextRouted.kernel !== routed.kernel)
|
|
4062
|
+
if (nextRouted.kernel !== routed.kernel) {
|
|
4063
|
+
pauseBeforeReject();
|
|
4064
|
+
throwPlayerError("E_METHOD_NOT_SUPPORTED", `换源需要从 ${routed.kernel} 内核切到 ${nextRouted.kernel},运行时无法切换内核。请销毁当前 player 并用新的 source 重建。`);
|
|
4065
|
+
}
|
|
4047
4066
|
const constructionChanges = sourceConstructionChanges(normalized, nextNormalized);
|
|
4048
|
-
if (constructionChanges.length > 0)
|
|
4067
|
+
if (constructionChanges.length > 0) {
|
|
4068
|
+
pauseBeforeReject();
|
|
4069
|
+
throwPlayerError("E_METHOD_NOT_SUPPORTED", `换源不能改变构造期 source 配置(${constructionChanges.join("、")})。播放器仍使用创建时的直播、HLS 与字幕插件配置。请销毁当前 player 并用新的 source 重建。`);
|
|
4070
|
+
}
|
|
4049
4071
|
const revision = ++loadRevision;
|
|
4050
4072
|
naturalProof?.abort();
|
|
4051
4073
|
recovery?.setSession(nextSessionId);
|
|
@@ -4070,7 +4092,15 @@ function createPlayer(options) {
|
|
|
4070
4092
|
if (destroyed || revision !== loadRevision) return;
|
|
4071
4093
|
emitContextChange();
|
|
4072
4094
|
if (destroyed || revision !== loadRevision) return;
|
|
4073
|
-
|
|
4095
|
+
settlePendingLoad?.();
|
|
4096
|
+
await new Promise((resolve) => {
|
|
4097
|
+
const settle = () => {
|
|
4098
|
+
if (settlePendingLoad === settle) settlePendingLoad = null;
|
|
4099
|
+
resolve();
|
|
4100
|
+
};
|
|
4101
|
+
settlePendingLoad = settle;
|
|
4102
|
+
Promise.resolve(player.switchURL(nextRouted.chosen.url)).then(settle, settle);
|
|
4103
|
+
});
|
|
4074
4104
|
if (destroyed || revision !== loadRevision) return;
|
|
4075
4105
|
getMediaSession(player)?.setMetadata(nextNormalized.metadata);
|
|
4076
4106
|
},
|
|
@@ -4094,6 +4124,7 @@ function createPlayer(options) {
|
|
|
4094
4124
|
*/
|
|
4095
4125
|
destroy() {
|
|
4096
4126
|
if (destroyed) return;
|
|
4127
|
+
settlePendingLoad?.();
|
|
4097
4128
|
recovery?.destroy();
|
|
4098
4129
|
recoveryCleanup?.();
|
|
4099
4130
|
destroyed = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@video-lab/player-core",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"xgplayer": "3.0.26",
|
|
34
34
|
"hls.js": "1.6.19",
|
|
35
35
|
"xgplayer-flv.js": "3.0.26",
|
|
36
|
-
"@video-lab/protocol": "4.1.
|
|
36
|
+
"@video-lab/protocol": "4.1.3"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"jsdom": "^24.0.0",
|