@video-lab/player-core 4.1.0 → 4.1.2
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 +44 -3
- package/dist/index.d.cts +10 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +44 -3
- package/package.json +3 -3
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)。
|
|
107
|
+
|
|
106
108
|
**直播的 `duration` 是 0,不是 Infinity。** Infinity 过不了 JSON 序列化(变成 null),而事件要跨 iframe 传,所以在源头就归一成 0。
|
|
107
109
|
|
|
108
110
|
## 环境探测与测试
|
package/dist/index.cjs
CHANGED
|
@@ -120,6 +120,27 @@ function detectEnv(input) {
|
|
|
120
120
|
requiresHls: isIOS || isWeChat || isUC || isQuark || !input.hasMediaSource
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
|
+
/** flv.js `isSupported()` 用的同一句基线:连它都报不支持,hls.js / flv.js 都不会开始播放 */
|
|
124
|
+
const MSE_BASELINE_TYPE = "video/mp4; codecs=\"avc1.42E01E,mp4a.40.2\"";
|
|
125
|
+
/**
|
|
126
|
+
* 标准 MSE 是否**可用**,而不只是存在(ADR-135)。
|
|
127
|
+
*
|
|
128
|
+
* 小米浏览器(MiuiBrowser 20.28)暴露了 `MediaSource`,但 `isTypeSupported` 对所有
|
|
129
|
+
* H.264/AAC 组合都回 false —— 实际 `addSourceBuffer` 能成功,是它报错了能力。
|
|
130
|
+
* hls.js 信了前者,对声明了 CODECS 的主清单报 `manifestIncompatibleCodecsError`(不可重试)。
|
|
131
|
+
* 按基线判为不可用后,SourceRouter 走原生 HLS,而该浏览器原生 HLS 播放、拖动都正常。
|
|
132
|
+
*
|
|
133
|
+
* 没有 `isTypeSupported` 的实现仍按「存在即可用」,只收紧「明确报不支持」这一种情况。
|
|
134
|
+
*/
|
|
135
|
+
function hasUsableMediaSource(mediaSource) {
|
|
136
|
+
if (typeof mediaSource === "undefined") return false;
|
|
137
|
+
if (typeof mediaSource.isTypeSupported !== "function") return true;
|
|
138
|
+
try {
|
|
139
|
+
return mediaSource.isTypeSupported(MSE_BASELINE_TYPE);
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
123
144
|
/**
|
|
124
145
|
* 从当前浏览器读环境。SSR 场景(没有 window / navigator)返回一个保守的默认值:
|
|
125
146
|
* 当作没有 MSE,于是强制走 HLS —— 宁可保守也不要在服务端渲染时误判成能播 FLV。
|
|
@@ -132,7 +153,7 @@ function detectEnvFromGlobals() {
|
|
|
132
153
|
});
|
|
133
154
|
return detectEnv({
|
|
134
155
|
userAgent: navigator.userAgent,
|
|
135
|
-
hasMediaSource:
|
|
156
|
+
hasMediaSource: hasUsableMediaSource(window.MediaSource),
|
|
136
157
|
hasManagedMediaSource: typeof window.ManagedMediaSource !== "undefined"
|
|
137
158
|
});
|
|
138
159
|
}
|
|
@@ -3154,8 +3175,15 @@ function createPlayer(options) {
|
|
|
3154
3175
|
let recovery = null;
|
|
3155
3176
|
let eventStreamReady = false;
|
|
3156
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";
|
|
3157
3185
|
const reportRecoveryFault = (event, eventSession) => {
|
|
3158
|
-
if (event.event === "error" &&
|
|
3186
|
+
if (event.event === "error" && isSourceFault(event, eventSession)) recovery?.fault(event.payload, eventSession);
|
|
3159
3187
|
};
|
|
3160
3188
|
const emit = (event) => {
|
|
3161
3189
|
if (destroyed) return;
|
|
@@ -3166,6 +3194,7 @@ function createPlayer(options) {
|
|
|
3166
3194
|
}
|
|
3167
3195
|
const eventSession = sessionId;
|
|
3168
3196
|
notify(delivered);
|
|
3197
|
+
if (isSourceFault(event, eventSession)) settlePendingLoad?.();
|
|
3169
3198
|
reportRecoveryFault(event, eventSession);
|
|
3170
3199
|
};
|
|
3171
3200
|
let playerRef = null;
|
|
@@ -4075,7 +4104,15 @@ function createPlayer(options) {
|
|
|
4075
4104
|
if (destroyed || revision !== loadRevision) return;
|
|
4076
4105
|
emitContextChange();
|
|
4077
4106
|
if (destroyed || revision !== loadRevision) return;
|
|
4078
|
-
|
|
4107
|
+
settlePendingLoad?.();
|
|
4108
|
+
await new Promise((resolve) => {
|
|
4109
|
+
const settle = () => {
|
|
4110
|
+
if (settlePendingLoad === settle) settlePendingLoad = null;
|
|
4111
|
+
resolve();
|
|
4112
|
+
};
|
|
4113
|
+
settlePendingLoad = settle;
|
|
4114
|
+
Promise.resolve(player.switchURL(nextRouted.chosen.url)).then(settle, settle);
|
|
4115
|
+
});
|
|
4079
4116
|
if (destroyed || revision !== loadRevision) return;
|
|
4080
4117
|
getMediaSession(player)?.setMetadata(nextNormalized.metadata);
|
|
4081
4118
|
},
|
|
@@ -4099,6 +4136,7 @@ function createPlayer(options) {
|
|
|
4099
4136
|
*/
|
|
4100
4137
|
destroy() {
|
|
4101
4138
|
if (destroyed) return;
|
|
4139
|
+
settlePendingLoad?.();
|
|
4102
4140
|
recovery?.destroy();
|
|
4103
4141
|
recoveryCleanup?.();
|
|
4104
4142
|
destroyed = true;
|
|
@@ -4199,6 +4237,9 @@ function buildXgplayerConfig(options, routed, normalized, emit, env, playableSta
|
|
|
4199
4237
|
const lowLatencyMode = normalized.hls?.lowLatencyMode === true;
|
|
4200
4238
|
return {
|
|
4201
4239
|
el: options.el,
|
|
4240
|
+
...options.fullscreenTarget ? { fullscreenTarget: options.fullscreenTarget } : {},
|
|
4241
|
+
width: "100%",
|
|
4242
|
+
height: "100%",
|
|
4202
4243
|
url: routed.chosen.url,
|
|
4203
4244
|
isLive: live,
|
|
4204
4245
|
ignores: [
|
package/dist/index.d.cts
CHANGED
|
@@ -78,6 +78,12 @@ declare function detectEnvFromGlobals(): Env;
|
|
|
78
78
|
interface CreatePlayerOptions {
|
|
79
79
|
/** 挂载容器 */
|
|
80
80
|
el: HTMLElement;
|
|
81
|
+
/**
|
|
82
|
+
* 原生全屏目标(ADR-133)。装配层传入同时包含播放器与 SDK 覆盖层的外层容器,
|
|
83
|
+
* 覆盖层才会进入 `document.fullscreenElement` 子树、在全屏期间可见。
|
|
84
|
+
* 不传时沿用 xgplayer 默认(播放器根元素)。内部装配参数,不属于 `PlayerConfig`。
|
|
85
|
+
*/
|
|
86
|
+
fullscreenTarget?: HTMLElement;
|
|
81
87
|
/** 宿主网页全屏布局;不经可序列化配置传递。 */
|
|
82
88
|
pageFullscreen?: PageFullscreenAdapter;
|
|
83
89
|
/** iframe 内部使用:等待宿主协商后开放入口。 */
|
|
@@ -153,6 +159,10 @@ interface PlayerCoreHandle {
|
|
|
153
159
|
* 安全热切媒体路由。改变内核、`live`、`hls.lowLatencyMode` 或字幕配置会抛
|
|
154
160
|
* `E_METHOD_NOT_SUPPORTED`,因为它们在播放器构造期确定;宿主须销毁后以新 source 重建。
|
|
155
161
|
* 组合层的常规换源入口是 `source` prop,直接调用只适合 player-core / 高级句柄场景。
|
|
162
|
+
*
|
|
163
|
+
* 返回的 Promise 在这次换源有结论时 resolve(ADR-136):新源起播、当前会话的源故障已经通过
|
|
164
|
+
* `error` 事件报出、被更新的 `load()` 取代或播放器销毁。resolve 不代表新源可播放;
|
|
165
|
+
* 新源加载失败只经 `error` 事件报告一次,不会让 `load()` reject。只有上面的校验失败才 reject。
|
|
156
166
|
*/
|
|
157
167
|
load(source: MediaSource): Promise<void>;
|
|
158
168
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -78,6 +78,12 @@ declare function detectEnvFromGlobals(): Env;
|
|
|
78
78
|
interface CreatePlayerOptions {
|
|
79
79
|
/** 挂载容器 */
|
|
80
80
|
el: HTMLElement;
|
|
81
|
+
/**
|
|
82
|
+
* 原生全屏目标(ADR-133)。装配层传入同时包含播放器与 SDK 覆盖层的外层容器,
|
|
83
|
+
* 覆盖层才会进入 `document.fullscreenElement` 子树、在全屏期间可见。
|
|
84
|
+
* 不传时沿用 xgplayer 默认(播放器根元素)。内部装配参数,不属于 `PlayerConfig`。
|
|
85
|
+
*/
|
|
86
|
+
fullscreenTarget?: HTMLElement;
|
|
81
87
|
/** 宿主网页全屏布局;不经可序列化配置传递。 */
|
|
82
88
|
pageFullscreen?: PageFullscreenAdapter;
|
|
83
89
|
/** iframe 内部使用:等待宿主协商后开放入口。 */
|
|
@@ -153,6 +159,10 @@ interface PlayerCoreHandle {
|
|
|
153
159
|
* 安全热切媒体路由。改变内核、`live`、`hls.lowLatencyMode` 或字幕配置会抛
|
|
154
160
|
* `E_METHOD_NOT_SUPPORTED`,因为它们在播放器构造期确定;宿主须销毁后以新 source 重建。
|
|
155
161
|
* 组合层的常规换源入口是 `source` prop,直接调用只适合 player-core / 高级句柄场景。
|
|
162
|
+
*
|
|
163
|
+
* 返回的 Promise 在这次换源有结论时 resolve(ADR-136):新源起播、当前会话的源故障已经通过
|
|
164
|
+
* `error` 事件报出、被更新的 `load()` 取代或播放器销毁。resolve 不代表新源可播放;
|
|
165
|
+
* 新源加载失败只经 `error` 事件报告一次,不会让 `load()` reject。只有上面的校验失败才 reject。
|
|
156
166
|
*/
|
|
157
167
|
load(source: MediaSource): Promise<void>;
|
|
158
168
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -94,6 +94,27 @@ function detectEnv(input) {
|
|
|
94
94
|
requiresHls: isIOS || isWeChat || isUC || isQuark || !input.hasMediaSource
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
+
/** flv.js `isSupported()` 用的同一句基线:连它都报不支持,hls.js / flv.js 都不会开始播放 */
|
|
98
|
+
const MSE_BASELINE_TYPE = "video/mp4; codecs=\"avc1.42E01E,mp4a.40.2\"";
|
|
99
|
+
/**
|
|
100
|
+
* 标准 MSE 是否**可用**,而不只是存在(ADR-135)。
|
|
101
|
+
*
|
|
102
|
+
* 小米浏览器(MiuiBrowser 20.28)暴露了 `MediaSource`,但 `isTypeSupported` 对所有
|
|
103
|
+
* H.264/AAC 组合都回 false —— 实际 `addSourceBuffer` 能成功,是它报错了能力。
|
|
104
|
+
* hls.js 信了前者,对声明了 CODECS 的主清单报 `manifestIncompatibleCodecsError`(不可重试)。
|
|
105
|
+
* 按基线判为不可用后,SourceRouter 走原生 HLS,而该浏览器原生 HLS 播放、拖动都正常。
|
|
106
|
+
*
|
|
107
|
+
* 没有 `isTypeSupported` 的实现仍按「存在即可用」,只收紧「明确报不支持」这一种情况。
|
|
108
|
+
*/
|
|
109
|
+
function hasUsableMediaSource(mediaSource) {
|
|
110
|
+
if (typeof mediaSource === "undefined") return false;
|
|
111
|
+
if (typeof mediaSource.isTypeSupported !== "function") return true;
|
|
112
|
+
try {
|
|
113
|
+
return mediaSource.isTypeSupported(MSE_BASELINE_TYPE);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
97
118
|
/**
|
|
98
119
|
* 从当前浏览器读环境。SSR 场景(没有 window / navigator)返回一个保守的默认值:
|
|
99
120
|
* 当作没有 MSE,于是强制走 HLS —— 宁可保守也不要在服务端渲染时误判成能播 FLV。
|
|
@@ -106,7 +127,7 @@ function detectEnvFromGlobals() {
|
|
|
106
127
|
});
|
|
107
128
|
return detectEnv({
|
|
108
129
|
userAgent: navigator.userAgent,
|
|
109
|
-
hasMediaSource:
|
|
130
|
+
hasMediaSource: hasUsableMediaSource(window.MediaSource),
|
|
110
131
|
hasManagedMediaSource: typeof window.ManagedMediaSource !== "undefined"
|
|
111
132
|
});
|
|
112
133
|
}
|
|
@@ -3128,8 +3149,15 @@ function createPlayer(options) {
|
|
|
3128
3149
|
let recovery = null;
|
|
3129
3150
|
let eventStreamReady = false;
|
|
3130
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";
|
|
3131
3159
|
const reportRecoveryFault = (event, eventSession) => {
|
|
3132
|
-
if (event.event === "error" &&
|
|
3160
|
+
if (event.event === "error" && isSourceFault(event, eventSession)) recovery?.fault(event.payload, eventSession);
|
|
3133
3161
|
};
|
|
3134
3162
|
const emit = (event) => {
|
|
3135
3163
|
if (destroyed) return;
|
|
@@ -3140,6 +3168,7 @@ function createPlayer(options) {
|
|
|
3140
3168
|
}
|
|
3141
3169
|
const eventSession = sessionId;
|
|
3142
3170
|
notify(delivered);
|
|
3171
|
+
if (isSourceFault(event, eventSession)) settlePendingLoad?.();
|
|
3143
3172
|
reportRecoveryFault(event, eventSession);
|
|
3144
3173
|
};
|
|
3145
3174
|
let playerRef = null;
|
|
@@ -4049,7 +4078,15 @@ function createPlayer(options) {
|
|
|
4049
4078
|
if (destroyed || revision !== loadRevision) return;
|
|
4050
4079
|
emitContextChange();
|
|
4051
4080
|
if (destroyed || revision !== loadRevision) return;
|
|
4052
|
-
|
|
4081
|
+
settlePendingLoad?.();
|
|
4082
|
+
await new Promise((resolve) => {
|
|
4083
|
+
const settle = () => {
|
|
4084
|
+
if (settlePendingLoad === settle) settlePendingLoad = null;
|
|
4085
|
+
resolve();
|
|
4086
|
+
};
|
|
4087
|
+
settlePendingLoad = settle;
|
|
4088
|
+
Promise.resolve(player.switchURL(nextRouted.chosen.url)).then(settle, settle);
|
|
4089
|
+
});
|
|
4053
4090
|
if (destroyed || revision !== loadRevision) return;
|
|
4054
4091
|
getMediaSession(player)?.setMetadata(nextNormalized.metadata);
|
|
4055
4092
|
},
|
|
@@ -4073,6 +4110,7 @@ function createPlayer(options) {
|
|
|
4073
4110
|
*/
|
|
4074
4111
|
destroy() {
|
|
4075
4112
|
if (destroyed) return;
|
|
4113
|
+
settlePendingLoad?.();
|
|
4076
4114
|
recovery?.destroy();
|
|
4077
4115
|
recoveryCleanup?.();
|
|
4078
4116
|
destroyed = true;
|
|
@@ -4173,6 +4211,9 @@ function buildXgplayerConfig(options, routed, normalized, emit, env, playableSta
|
|
|
4173
4211
|
const lowLatencyMode = normalized.hls?.lowLatencyMode === true;
|
|
4174
4212
|
return {
|
|
4175
4213
|
el: options.el,
|
|
4214
|
+
...options.fullscreenTarget ? { fullscreenTarget: options.fullscreenTarget } : {},
|
|
4215
|
+
width: "100%",
|
|
4216
|
+
height: "100%",
|
|
4176
4217
|
url: routed.chosen.url,
|
|
4177
4218
|
isLive: live,
|
|
4178
4219
|
ignores: [
|
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.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"sideEffects": false,
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"xgplayer": "3.0.26",
|
|
34
|
-
"hls.js": "1.6.
|
|
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.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"jsdom": "^24.0.0",
|