@video-lab/react-frame 3.1.0 → 4.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 +146 -15
- package/dist/index.cjs +581 -45
- package/dist/index.d.cts +25 -8
- package/dist/index.d.mts +25 -8
- package/dist/index.mjs +571 -47
- package/package.json +4 -3
- package/skills/host-integration/SKILL.md +6 -7
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _video_lab_react_config = require("@video-lab/react-config");
|
|
3
|
+
let _video_lab_protocol = require("@video-lab/protocol");
|
|
2
4
|
let react = require("react");
|
|
3
5
|
let _video_lab_frame_core = require("@video-lab/frame-core");
|
|
4
6
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
7
|
//#region src/use-frame-connection.ts
|
|
8
|
+
let hostProducerSequence = 0;
|
|
6
9
|
/**
|
|
7
10
|
* 管理 frame-core 连接的整个生命周期,并把 iframe 事件桥接到 React 回调 props。
|
|
8
11
|
*
|
|
@@ -13,33 +16,319 @@ let react_jsx_runtime = require("react/jsx-runtime");
|
|
|
13
16
|
*
|
|
14
17
|
* SSR 安全:`createPlayerFrame` 只在 useEffect(客户端)里跑,iframe 创建天然被挡在 SSR 之外。
|
|
15
18
|
*/
|
|
16
|
-
function useFrameConnection(props) {
|
|
19
|
+
function useFrameConnection(props, hasCustomPoster = false, hasCustomPause = false) {
|
|
17
20
|
const containerRef = (0, react.useRef)(null);
|
|
18
21
|
const handleRef = (0, react.useRef)(null);
|
|
19
22
|
const [mounted, setMounted] = (0, react.useState)(false);
|
|
23
|
+
const [playIntent, setPlayIntent] = (0, react.useState)((props.autoplay ?? (props.preset ? _video_lab_protocol.PRESETS[props.preset].autoplay : false)) === true || props.preload === "auto");
|
|
24
|
+
const [rebuildKey, setRebuildKey] = (0, react.useState)(0);
|
|
25
|
+
const [sourceOverride, setSourceOverride] = (0, react.useState)(null);
|
|
26
|
+
const [sourceSwitching, setSourceSwitching] = (0, react.useState)(false);
|
|
27
|
+
const [hostOwnsOverlays, setHostOwnsOverlays] = (0, react.useState)(false);
|
|
28
|
+
const [terminalError, setTerminalError] = (0, react.useState)(null);
|
|
29
|
+
const terminalRef = (0, react.useRef)(null);
|
|
30
|
+
const diagnosticRef = (0, react.useRef)(null);
|
|
31
|
+
const actionBusyRef = (0, react.useRef)(false);
|
|
20
32
|
const [hostPosterVisible, setHostPosterVisible] = (0, react.useState)(true);
|
|
33
|
+
const [hostPauseVisible, setHostPauseVisible] = (0, react.useState)(false);
|
|
34
|
+
const pauseIntentPendingRef = (0, react.useRef)(false);
|
|
35
|
+
const firstFrameSeenRef = (0, react.useRef)(false);
|
|
36
|
+
const awaitingNewSourceRef = (0, react.useRef)(false);
|
|
37
|
+
const [loadingOverlay, setLoadingOverlay] = (0, react.useState)({
|
|
38
|
+
phase: "initial",
|
|
39
|
+
reason: "frame_disconnected"
|
|
40
|
+
});
|
|
21
41
|
const propsRef = (0, react.useRef)(props);
|
|
22
42
|
propsRef.current = props;
|
|
43
|
+
const activeSourceRef = (0, react.useRef)(props.source);
|
|
44
|
+
activeSourceRef.current = sourceOverride ?? props.source;
|
|
45
|
+
const sourceSwitchRef = (0, react.useRef)(null);
|
|
46
|
+
const sourceSwitchSequence = (0, react.useRef)(0);
|
|
47
|
+
const promptSourceGeneration = (0, react.useRef)(0);
|
|
48
|
+
const playingIntentRef = (0, react.useRef)((props.autoplay ?? (props.preset ? _video_lab_protocol.PRESETS[props.preset].autoplay : false)) === true);
|
|
49
|
+
const hostDeliveryRef = (0, react.useRef)(null);
|
|
50
|
+
const forwardWithDelivery = (0, react.useCallback)((event, currentProps) => {
|
|
51
|
+
if (event.delivery) {
|
|
52
|
+
forward(event, currentProps);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
hostDeliveryRef.current ??= {
|
|
56
|
+
sessionId: `react-frame-${Date.now()}-${++hostProducerSequence}`,
|
|
57
|
+
sequence: 0
|
|
58
|
+
};
|
|
59
|
+
const producer = hostDeliveryRef.current;
|
|
60
|
+
producer.sequence += 1;
|
|
61
|
+
forward({
|
|
62
|
+
...event,
|
|
63
|
+
delivery: {
|
|
64
|
+
producerSessionId: producer.sessionId,
|
|
65
|
+
deliveryId: `${producer.sessionId}:${producer.sequence}`,
|
|
66
|
+
occurredAtMs: Date.now(),
|
|
67
|
+
sequence: producer.sequence
|
|
68
|
+
}
|
|
69
|
+
}, currentProps);
|
|
70
|
+
}, []);
|
|
71
|
+
const cancelSourceSwitch = (reason = "unmounted") => {
|
|
72
|
+
const transaction = sourceSwitchRef.current;
|
|
73
|
+
transaction?.controller.abort();
|
|
74
|
+
sourceSwitchRef.current = null;
|
|
75
|
+
setSourceSwitching(false);
|
|
76
|
+
if (!transaction) return;
|
|
77
|
+
handleRef.current?.setSourceSwitching(false).catch(() => {});
|
|
78
|
+
forwardWithDelivery({
|
|
79
|
+
event: "sourceswitch",
|
|
80
|
+
payload: {
|
|
81
|
+
phase: "cancelled",
|
|
82
|
+
switchId: transaction.id,
|
|
83
|
+
reason,
|
|
84
|
+
elapsedMs: sourceSwitchElapsed(transaction.startedAt)
|
|
85
|
+
}
|
|
86
|
+
}, propsRef.current);
|
|
87
|
+
};
|
|
88
|
+
const endSourceSwitchAsError = (id, reason, errorCode) => {
|
|
89
|
+
const transaction = sourceSwitchRef.current;
|
|
90
|
+
if (transaction?.id !== id) return;
|
|
91
|
+
sourceSwitchRef.current = null;
|
|
92
|
+
setSourceSwitching(false);
|
|
93
|
+
awaitingNewSourceRef.current = false;
|
|
94
|
+
handleRef.current?.setSourceSwitching(false).catch(() => {});
|
|
95
|
+
forwardWithDelivery({
|
|
96
|
+
event: "sourceswitch",
|
|
97
|
+
payload: {
|
|
98
|
+
phase: "failed",
|
|
99
|
+
switchId: transaction.id,
|
|
100
|
+
reason,
|
|
101
|
+
elapsedMs: sourceSwitchElapsed(transaction.startedAt),
|
|
102
|
+
...errorCode === void 0 ? {} : { errorCode }
|
|
103
|
+
}
|
|
104
|
+
}, propsRef.current);
|
|
105
|
+
forwardWithDelivery({
|
|
106
|
+
event: "playablechange",
|
|
107
|
+
payload: {
|
|
108
|
+
playable: false,
|
|
109
|
+
recoverable: false,
|
|
110
|
+
reason: "error",
|
|
111
|
+
action: "replace-source"
|
|
112
|
+
}
|
|
113
|
+
}, propsRef.current);
|
|
114
|
+
const terminal = {
|
|
115
|
+
error: diagnosticRef.current ?? (0, _video_lab_protocol.makePlayerError)("E_AUTH_EXPIRED", ""),
|
|
116
|
+
action: "replace-source",
|
|
117
|
+
reason: "error",
|
|
118
|
+
synthetic: true
|
|
119
|
+
};
|
|
120
|
+
terminalRef.current = terminal;
|
|
121
|
+
setTerminalError(terminal);
|
|
122
|
+
};
|
|
123
|
+
const restoreSourceSwitchOnReady = () => {
|
|
124
|
+
const transaction = sourceSwitchRef.current;
|
|
125
|
+
const current = handleRef.current;
|
|
126
|
+
if (!transaction?.loadAccepted || !current) return;
|
|
127
|
+
sourceSwitchRef.current = null;
|
|
128
|
+
setSourceSwitching(false);
|
|
129
|
+
handleRef.current?.setSourceSwitching(false).catch(() => {});
|
|
130
|
+
forwardWithDelivery({
|
|
131
|
+
event: "sourceswitch",
|
|
132
|
+
payload: {
|
|
133
|
+
phase: "succeeded",
|
|
134
|
+
switchId: transaction.id,
|
|
135
|
+
strategy: transaction.strategy,
|
|
136
|
+
elapsedMs: sourceSwitchElapsed(transaction.startedAt)
|
|
137
|
+
}
|
|
138
|
+
}, propsRef.current);
|
|
139
|
+
if (transaction.restorePosition !== null) current.seek(transaction.restorePosition).catch(() => {});
|
|
140
|
+
if (transaction.wasPlaying) current.play().catch(() => {});
|
|
141
|
+
};
|
|
142
|
+
const startSourceSwitch = (cause) => {
|
|
143
|
+
const resolveSource = propsRef.current.resolveSource;
|
|
144
|
+
if (cause.code !== "E_AUTH_EXPIRED" || !resolveSource || sourceSwitchRef.current) return false;
|
|
145
|
+
const id = ++sourceSwitchSequence.current;
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const context = handleRef.current?.getPlaybackContext();
|
|
148
|
+
sourceSwitchRef.current = {
|
|
149
|
+
controller,
|
|
150
|
+
id,
|
|
151
|
+
restorePosition: context?.streamKind === "vod" && Number.isFinite(context.position) ? context.position : null,
|
|
152
|
+
wasPlaying: playingIntentRef.current,
|
|
153
|
+
loadAccepted: false,
|
|
154
|
+
startedAt: sourceSwitchNow(),
|
|
155
|
+
strategy: "hot_load"
|
|
156
|
+
};
|
|
157
|
+
const httpStatus = cause.cause?.status;
|
|
158
|
+
forwardWithDelivery({
|
|
159
|
+
event: "sourceswitch",
|
|
160
|
+
payload: {
|
|
161
|
+
phase: "started",
|
|
162
|
+
switchId: id,
|
|
163
|
+
trigger: "auth_expired",
|
|
164
|
+
...httpStatus === 401 || httpStatus === 403 ? { httpStatus } : {}
|
|
165
|
+
}
|
|
166
|
+
}, propsRef.current);
|
|
167
|
+
setSourceSwitching(true);
|
|
168
|
+
pauseIntentPendingRef.current = false;
|
|
169
|
+
setHostPauseVisible(false);
|
|
170
|
+
awaitingNewSourceRef.current = "switching";
|
|
171
|
+
handleRef.current?.setSourceSwitching(true).catch(() => {});
|
|
172
|
+
forwardWithDelivery({
|
|
173
|
+
event: "playablechange",
|
|
174
|
+
payload: {
|
|
175
|
+
playable: false,
|
|
176
|
+
recoverable: true,
|
|
177
|
+
reason: "source_switching",
|
|
178
|
+
action: "none"
|
|
179
|
+
}
|
|
180
|
+
}, propsRef.current);
|
|
181
|
+
Promise.resolve().then(() => resolveSource({
|
|
182
|
+
cause,
|
|
183
|
+
signal: controller.signal
|
|
184
|
+
})).then((source) => {
|
|
185
|
+
if (sourceSwitchRef.current?.id !== id || controller.signal.aborted) return;
|
|
186
|
+
if (source === null || !handleRef.current) return endSourceSwitchAsError(id, "resolver_null");
|
|
187
|
+
sourceSwitchRef.current.loadAccepted = true;
|
|
188
|
+
handleRef.current.load(source).then(() => {}, (error) => {
|
|
189
|
+
const playerError = (0, _video_lab_frame_core.toPlayerError)(error);
|
|
190
|
+
if (playerError.code !== "E_METHOD_NOT_SUPPORTED") return endSourceSwitchAsError(id, "load_rejected", playerError.code);
|
|
191
|
+
if (sourceSwitchRef.current?.id !== id || controller.signal.aborted) return;
|
|
192
|
+
sourceSwitchRef.current.strategy = "rebuild";
|
|
193
|
+
setSourceOverride(source);
|
|
194
|
+
setRebuildKey((key) => key + 1);
|
|
195
|
+
});
|
|
196
|
+
}).catch(() => {
|
|
197
|
+
if (!controller.signal.aborted) endSourceSwitchAsError(id, "resolver_rejected");
|
|
198
|
+
});
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
const performErrorAction = async () => {
|
|
202
|
+
if (actionBusyRef.current || sourceSwitchRef.current) return "busy";
|
|
203
|
+
const current = handleRef.current;
|
|
204
|
+
const terminal = terminalRef.current;
|
|
205
|
+
if (!current || !terminal) return "unavailable";
|
|
206
|
+
const action = (0, _video_lab_protocol.effectiveErrorAction)({
|
|
207
|
+
reason: terminal.reason,
|
|
208
|
+
recoverable: false,
|
|
209
|
+
action: terminal.action,
|
|
210
|
+
errorCode: terminal.error.code,
|
|
211
|
+
canResolveSource: Boolean(propsRef.current.resolveSource),
|
|
212
|
+
canRecreateFrame: true
|
|
213
|
+
});
|
|
214
|
+
if (action === "none") return "unavailable";
|
|
215
|
+
if (action === "replace-source") return startSourceSwitch(terminal.error) ? "started" : "unavailable";
|
|
216
|
+
actionBusyRef.current = true;
|
|
217
|
+
try {
|
|
218
|
+
if (action === "recreate-frame") {
|
|
219
|
+
await current.destroy();
|
|
220
|
+
handleRef.current = null;
|
|
221
|
+
terminalRef.current = null;
|
|
222
|
+
setTerminalError(null);
|
|
223
|
+
diagnosticRef.current = null;
|
|
224
|
+
setHostOwnsOverlays(false);
|
|
225
|
+
firstFrameSeenRef.current = false;
|
|
226
|
+
awaitingNewSourceRef.current = false;
|
|
227
|
+
setHostPosterVisible(true);
|
|
228
|
+
setHostPauseVisible(false);
|
|
229
|
+
setLoadingOverlay({
|
|
230
|
+
phase: "initial",
|
|
231
|
+
reason: "frame_disconnected"
|
|
232
|
+
});
|
|
233
|
+
setRebuildKey((key) => key + 1);
|
|
234
|
+
} else if (action === "play") await current.play();
|
|
235
|
+
else await current.retry();
|
|
236
|
+
return "started";
|
|
237
|
+
} finally {
|
|
238
|
+
actionBusyRef.current = false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
23
241
|
(0, react.useEffect)(() => {
|
|
24
242
|
let cancelled = false;
|
|
25
243
|
const container = containerRef.current;
|
|
26
244
|
if (!container) return;
|
|
27
245
|
const p = propsRef.current;
|
|
246
|
+
setHostOwnsOverlays(false);
|
|
28
247
|
(0, _video_lab_frame_core.createPlayerFrame)({
|
|
29
248
|
container,
|
|
30
|
-
source:
|
|
31
|
-
config: toConfig(p),
|
|
249
|
+
source: activeSourceRef.current,
|
|
250
|
+
config: toConfig(p, hasCustomPoster, hasCustomPause),
|
|
251
|
+
renderDefaultFallback: false,
|
|
252
|
+
onHostOverlayOwnershipChange: (owned) => setHostOwnsOverlays(owned),
|
|
253
|
+
sourceSwitching: sourceSwitchRef.current !== null,
|
|
32
254
|
pageFullscreen: p.pageFullscreen,
|
|
33
255
|
...p.origin !== void 0 ? { origin: p.origin } : {},
|
|
34
256
|
...p.iframeVersion !== void 0 ? { iframeVersion: p.iframeVersion } : {},
|
|
35
257
|
onEvent: (event) => {
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
258
|
+
if (cancelled) return;
|
|
259
|
+
if (event.event === "useraction" && event.payload.action === "switch_play_pause") {
|
|
260
|
+
if (event.payload.to === false) pauseIntentPendingRef.current = false;
|
|
261
|
+
else if (event.payload.to === true || event.payload.to === null && playingIntentRef.current) pauseIntentPendingRef.current = true;
|
|
262
|
+
}
|
|
263
|
+
if (event.event === "contextchange") {
|
|
264
|
+
pauseIntentPendingRef.current = false;
|
|
265
|
+
setHostPauseVisible(false);
|
|
266
|
+
}
|
|
267
|
+
if (event.event === "pause") {
|
|
268
|
+
setHostPauseVisible(pauseIntentPendingRef.current);
|
|
269
|
+
pauseIntentPendingRef.current = false;
|
|
270
|
+
}
|
|
271
|
+
if (event.event === "play" || event.event === "ended") {
|
|
272
|
+
pauseIntentPendingRef.current = false;
|
|
273
|
+
setHostPauseVisible(false);
|
|
274
|
+
}
|
|
275
|
+
if (event.event === "play" || event.event === "playing") {
|
|
276
|
+
playingIntentRef.current = true;
|
|
277
|
+
setPlayIntent(true);
|
|
278
|
+
}
|
|
279
|
+
if (event.event === "pause" || event.event === "ended") {
|
|
280
|
+
playingIntentRef.current = false;
|
|
281
|
+
setPlayIntent(false);
|
|
282
|
+
}
|
|
283
|
+
if (event.event === "error") {
|
|
284
|
+
diagnosticRef.current = event.payload;
|
|
285
|
+
const active = sourceSwitchRef.current;
|
|
286
|
+
if (active?.loadAccepted) endSourceSwitchAsError(active.id, "replacement_error", event.payload.code);
|
|
287
|
+
startSourceSwitch(event.payload);
|
|
288
|
+
}
|
|
289
|
+
if (event.event === "ready") {
|
|
290
|
+
if (awaitingNewSourceRef.current === "switching") awaitingNewSourceRef.current = "new-source";
|
|
291
|
+
restoreSourceSwitchOnReady();
|
|
292
|
+
}
|
|
293
|
+
if (event.event === "playablechange" && event.payload.reason === "error" && sourceSwitchRef.current) return;
|
|
294
|
+
if (event.event === "playablechange") {
|
|
295
|
+
const { playable, recoverable, reason, action = "none" } = event.payload;
|
|
296
|
+
if (!playable && !recoverable && (reason === "error" || reason === "frame_disconnected" || reason === "autoplay_blocked")) {
|
|
297
|
+
const terminal = {
|
|
298
|
+
error: reason === "autoplay_blocked" ? (0, _video_lab_protocol.makePlayerError)("E_AUTOPLAY_BLOCKED", "") : diagnosticRef.current ?? (0, _video_lab_protocol.makePlayerError)("E_UNKNOWN", ""),
|
|
299
|
+
action,
|
|
300
|
+
reason
|
|
301
|
+
};
|
|
302
|
+
terminalRef.current = terminal;
|
|
303
|
+
setTerminalError(terminal);
|
|
304
|
+
pauseIntentPendingRef.current = false;
|
|
305
|
+
setHostPauseVisible(false);
|
|
306
|
+
} else if (playable || recoverable) {
|
|
307
|
+
terminalRef.current = null;
|
|
308
|
+
setTerminalError(null);
|
|
309
|
+
if (playable) diagnosticRef.current = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (event.event === "firstframe" || event.event === "timeupdate") if (awaitingNewSourceRef.current === "switching") {} else {
|
|
313
|
+
if (awaitingNewSourceRef.current === "new-source") awaitingNewSourceRef.current = false;
|
|
314
|
+
firstFrameSeenRef.current = true;
|
|
315
|
+
setHostPosterVisible(false);
|
|
316
|
+
setLoadingOverlay(null);
|
|
317
|
+
}
|
|
318
|
+
else if (event.event === "playablechange") {
|
|
319
|
+
const { playable, recoverable, reason, action = "none" } = event.payload;
|
|
320
|
+
const terminalError = !playable && !recoverable && reason === "error";
|
|
321
|
+
const blockingAction = !playable && !recoverable && action !== "none";
|
|
322
|
+
if (terminalError) setHostPosterVisible(false);
|
|
323
|
+
setLoadingOverlay(!firstFrameSeenRef.current ? terminalError || blockingAction ? null : {
|
|
324
|
+
phase: "initial",
|
|
325
|
+
reason
|
|
326
|
+
} : !playable && recoverable && reason !== "initializing" ? {
|
|
327
|
+
phase: "runtime",
|
|
328
|
+
reason
|
|
329
|
+
} : null);
|
|
330
|
+
}
|
|
331
|
+
forwardWithDelivery(event, propsRef.current);
|
|
43
332
|
},
|
|
44
333
|
debug: p.debug ?? false
|
|
45
334
|
}).then((h) => {
|
|
@@ -49,47 +338,124 @@ function useFrameConnection(props) {
|
|
|
49
338
|
}
|
|
50
339
|
handleRef.current = h;
|
|
51
340
|
setMounted(true);
|
|
341
|
+
if (propsRef.current.showErrorOverlay !== p.showErrorOverlay || propsRef.current.errorButtonColors !== p.errorButtonColors) h.setOverlayPresentation({
|
|
342
|
+
showErrorOverlay: propsRef.current.showErrorOverlay !== false,
|
|
343
|
+
...propsRef.current.errorButtonColors ? { errorButtonColors: propsRef.current.errorButtonColors } : {}
|
|
344
|
+
}).catch(() => {});
|
|
345
|
+
const latestLocale = propsRef.current.locale;
|
|
346
|
+
if (latestLocale !== void 0 && JSON.stringify(latestLocale) !== JSON.stringify(p.locale)) h.setLocale(latestLocale).catch((err) => forwardWithDelivery({
|
|
347
|
+
event: "error",
|
|
348
|
+
payload: (0, _video_lab_frame_core.toPlayerError)(err)
|
|
349
|
+
}, propsRef.current));
|
|
52
350
|
});
|
|
53
351
|
return () => {
|
|
54
352
|
cancelled = true;
|
|
55
353
|
handleRef.current?.destroy();
|
|
56
354
|
handleRef.current = null;
|
|
57
355
|
};
|
|
58
|
-
}, []);
|
|
356
|
+
}, [rebuildKey]);
|
|
357
|
+
(0, react.useEffect)(() => cancelSourceSwitch, []);
|
|
59
358
|
const sourceKey = JSON.stringify(props.source);
|
|
60
359
|
(0, react.useEffect)(() => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
360
|
+
cancelSourceSwitch("external_source");
|
|
361
|
+
setSourceOverride(null);
|
|
362
|
+
const generation = ++promptSourceGeneration.current;
|
|
363
|
+
handleRef.current?.load(props.source).catch((err) => {
|
|
364
|
+
const playerError = (0, _video_lab_frame_core.toPlayerError)(err);
|
|
365
|
+
if (promptSourceGeneration.current !== generation) {
|
|
366
|
+
forwardWithDelivery({
|
|
367
|
+
event: "error",
|
|
368
|
+
payload: playerError
|
|
369
|
+
}, propsRef.current);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
diagnosticRef.current = playerError;
|
|
373
|
+
const terminal = {
|
|
374
|
+
error: playerError,
|
|
375
|
+
action: "replace-source",
|
|
376
|
+
reason: "error",
|
|
377
|
+
synthetic: true
|
|
378
|
+
};
|
|
379
|
+
terminalRef.current = terminal;
|
|
380
|
+
setTerminalError(terminal);
|
|
381
|
+
awaitingNewSourceRef.current = false;
|
|
382
|
+
setHostPosterVisible(false);
|
|
383
|
+
setHostPauseVisible(false);
|
|
384
|
+
setLoadingOverlay(null);
|
|
385
|
+
forwardWithDelivery({
|
|
386
|
+
event: "error",
|
|
387
|
+
payload: playerError
|
|
388
|
+
}, propsRef.current);
|
|
389
|
+
});
|
|
65
390
|
}, [sourceKey]);
|
|
66
391
|
(0, react.useEffect)(() => {
|
|
67
|
-
if (props.muted !== void 0) handleRef.current?.setMuted(props.muted).catch((err) =>
|
|
392
|
+
if (props.muted !== void 0) handleRef.current?.setMuted(props.muted).catch((err) => forwardWithDelivery({
|
|
68
393
|
event: "error",
|
|
69
394
|
payload: (0, _video_lab_frame_core.toPlayerError)(err)
|
|
70
395
|
}, propsRef.current));
|
|
71
|
-
}, [props.muted]);
|
|
396
|
+
}, [props.muted, forwardWithDelivery]);
|
|
72
397
|
(0, react.useEffect)(() => {
|
|
73
|
-
if (props.locale !== void 0) handleRef.current?.setLocale(props.locale).catch((err) =>
|
|
398
|
+
if (props.locale !== void 0) handleRef.current?.setLocale(props.locale).catch((err) => forwardWithDelivery({
|
|
74
399
|
event: "error",
|
|
75
400
|
payload: (0, _video_lab_frame_core.toPlayerError)(err)
|
|
76
401
|
}, propsRef.current));
|
|
77
|
-
}, [props.locale]);
|
|
402
|
+
}, [props.locale, forwardWithDelivery]);
|
|
403
|
+
(0, react.useEffect)(() => {
|
|
404
|
+
if (!hostOwnsOverlays) return;
|
|
405
|
+
handleRef.current?.setOverlayPresentation({
|
|
406
|
+
showErrorOverlay: props.showErrorOverlay !== false,
|
|
407
|
+
...props.errorButtonColors ? { errorButtonColors: props.errorButtonColors } : {}
|
|
408
|
+
}).catch(() => {});
|
|
409
|
+
}, [
|
|
410
|
+
hostOwnsOverlays,
|
|
411
|
+
props.showErrorOverlay,
|
|
412
|
+
props.errorButtonColors
|
|
413
|
+
]);
|
|
414
|
+
const previousSourceKeyRef = (0, react.useRef)(sourceKey);
|
|
78
415
|
(0, react.useEffect)(() => {
|
|
416
|
+
if (previousSourceKeyRef.current === sourceKey) return;
|
|
417
|
+
previousSourceKeyRef.current = sourceKey;
|
|
79
418
|
setHostPosterVisible(true);
|
|
419
|
+
pauseIntentPendingRef.current = false;
|
|
420
|
+
setHostPauseVisible(false);
|
|
421
|
+
firstFrameSeenRef.current = false;
|
|
422
|
+
awaitingNewSourceRef.current = "switching";
|
|
423
|
+
setLoadingOverlay({
|
|
424
|
+
phase: "initial",
|
|
425
|
+
reason: "initializing"
|
|
426
|
+
});
|
|
80
427
|
}, [sourceKey]);
|
|
81
428
|
return {
|
|
82
429
|
containerRef,
|
|
83
430
|
handleRef,
|
|
84
431
|
mounted,
|
|
85
|
-
hostPosterVisible
|
|
432
|
+
hostPosterVisible,
|
|
433
|
+
hostPauseVisible,
|
|
434
|
+
pause: (0, react.useCallback)(async () => {
|
|
435
|
+
const current = handleRef.current;
|
|
436
|
+
if (!current) return;
|
|
437
|
+
pauseIntentPendingRef.current = true;
|
|
438
|
+
try {
|
|
439
|
+
await current.pause();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
pauseIntentPendingRef.current = false;
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
}, []),
|
|
445
|
+
playIntent,
|
|
446
|
+
loadingOverlay,
|
|
447
|
+
terminalError,
|
|
448
|
+
hostOwnsOverlays,
|
|
449
|
+
performErrorAction,
|
|
450
|
+
sourceSwitching,
|
|
451
|
+
cancelSourceSwitch
|
|
86
452
|
};
|
|
87
453
|
}
|
|
88
454
|
/**
|
|
89
455
|
* 只把"显式传了的"字段带上;undefined 交给 iframe 侧走 preset / xgplayer 默认。
|
|
90
456
|
* 和 vue-frame 的 toConfig 语义一致(resolvePreset 的语义就是 undefined = 没传)。
|
|
91
457
|
*/
|
|
92
|
-
function toConfig(props) {
|
|
458
|
+
function toConfig(props, hasCustomPoster = false, hasCustomPause = false) {
|
|
93
459
|
const config = {};
|
|
94
460
|
if (props.preset !== void 0) config.preset = props.preset;
|
|
95
461
|
if (props.autoplay !== void 0) config.autoplay = props.autoplay;
|
|
@@ -102,23 +468,32 @@ function toConfig(props) {
|
|
|
102
468
|
if (props.preload !== void 0) config.preload = props.preload;
|
|
103
469
|
if (props.controls !== void 0) config.controls = props.controls;
|
|
104
470
|
if (props.showErrorOverlay !== void 0) config.showErrorOverlay = props.showErrorOverlay;
|
|
105
|
-
if (props.
|
|
471
|
+
if (props.renderLoading !== void 0) config.showDefaultLoadingOverlay = false;
|
|
472
|
+
else if (props.showDefaultLoadingOverlay !== void 0) config.showDefaultLoadingOverlay = props.showDefaultLoadingOverlay;
|
|
473
|
+
if (props.showLoadingText !== void 0) config.showLoadingText = props.showLoadingText;
|
|
474
|
+
if (props.errorButtonColors !== void 0) config.errorButtonColors = props.errorButtonColors;
|
|
106
475
|
if (props.controlVisibility !== void 0) config.controlVisibility = props.controlVisibility;
|
|
107
476
|
if (props.interactive !== void 0) config.interactive = props.interactive;
|
|
108
|
-
if (props.poster
|
|
109
|
-
if (props.pauseImage
|
|
477
|
+
if (!hasCustomPoster && props.poster != null) config.poster = props.poster;
|
|
478
|
+
if (!hasCustomPause && props.pauseImage != null) config.pauseImage = props.pauseImage;
|
|
110
479
|
if (props.locale !== void 0) config.locale = props.locale;
|
|
111
480
|
if (props.danmaku !== void 0) config.danmaku = props.danmaku;
|
|
112
481
|
if (props.debug !== void 0) config.debug = props.debug;
|
|
113
482
|
return config;
|
|
114
483
|
}
|
|
484
|
+
function sourceSwitchNow() {
|
|
485
|
+
return globalThis.performance?.now?.() ?? Date.now();
|
|
486
|
+
}
|
|
487
|
+
function sourceSwitchElapsed(startedAt) {
|
|
488
|
+
return Math.max(0, sourceSwitchNow() - startedAt);
|
|
489
|
+
}
|
|
115
490
|
/**
|
|
116
491
|
* iframe 事件 → React 回调。名字从 wire 上的小写拼写换成 `onCamelCase`。
|
|
117
492
|
* 和 vue-frame 的 forward 一一对应(cross-mode-parity):同样的事件、同样的 payload 裁剪。
|
|
118
493
|
*
|
|
119
|
-
* **契约声明的
|
|
494
|
+
* **契约声明的 28 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
|
|
120
495
|
* 静态断言。曾经漏过 seeking / seeked / waiting / playing 四个,理由是"团队封装层没用到" ——
|
|
121
|
-
* 但静态 iframe 模式是裸广播、
|
|
496
|
+
* 但静态 iframe 模式是裸广播、28 个全发,于是同一份契约在四种接入方式下能收到的事件
|
|
122
497
|
* 不一样,parity 红线实际已破。别再以"业务暂时用不到"为由少接一个。
|
|
123
498
|
*/
|
|
124
499
|
function forward(event, props) {
|
|
@@ -204,7 +579,8 @@ function forward(event, props) {
|
|
|
204
579
|
case "audiohealth":
|
|
205
580
|
props.onAudioHealth?.(event.payload);
|
|
206
581
|
break;
|
|
207
|
-
case "recovery":
|
|
582
|
+
case "recovery":
|
|
583
|
+
case "sourceswitch": break;
|
|
208
584
|
default: break;
|
|
209
585
|
}
|
|
210
586
|
}
|
|
@@ -218,7 +594,7 @@ function forward(event, props) {
|
|
|
218
594
|
* `loading="lazy"` 在这里不影响抢首帧:它只推迟**视口外**的图,而封面天然在视口内。
|
|
219
595
|
*/
|
|
220
596
|
function HostPoster({ poster }) {
|
|
221
|
-
if (poster
|
|
597
|
+
if (poster == null) return null;
|
|
222
598
|
const url = typeof poster === "string" ? poster : poster.url;
|
|
223
599
|
if (!url) return null;
|
|
224
600
|
const fit = typeof poster === "string" ? void 0 : poster.fit;
|
|
@@ -234,7 +610,8 @@ function HostPoster({ poster }) {
|
|
|
234
610
|
height: "100%",
|
|
235
611
|
display: "block",
|
|
236
612
|
objectFit: fit ?? "cover",
|
|
237
|
-
zIndex: 10
|
|
613
|
+
zIndex: 10,
|
|
614
|
+
pointerEvents: "none"
|
|
238
615
|
}
|
|
239
616
|
});
|
|
240
617
|
}
|
|
@@ -254,11 +631,16 @@ function HostPoster({ poster }) {
|
|
|
254
631
|
* <VideoPlayerFrame ref={ref} source="https://cdn/a.m3u8" muted onReady={...} />
|
|
255
632
|
* ref.current?.play()
|
|
256
633
|
*/
|
|
257
|
-
const VideoPlayerFrame = (0, react.forwardRef)(function VideoPlayerFrame(
|
|
258
|
-
const
|
|
634
|
+
const VideoPlayerFrame = (0, react.forwardRef)(function VideoPlayerFrame(rawProps, ref) {
|
|
635
|
+
const props = (0, _video_lab_react_config.useResolvedPlayerProps)(rawProps, true);
|
|
636
|
+
const [hasCustomPoster] = (0, react.useState)(() => props.renderPoster !== void 0);
|
|
637
|
+
const [hasCustomPause] = (0, react.useState)(() => props.renderPause !== void 0);
|
|
638
|
+
const [localeOverride, setLocaleOverride] = (0, react.useState)();
|
|
639
|
+
(0, react.useEffect)(() => setLocaleOverride(void 0), [JSON.stringify(props.locale)]);
|
|
640
|
+
const { containerRef, handleRef, mounted, hostPosterVisible, hostPauseVisible, pause, playIntent, loadingOverlay, terminalError, hostOwnsOverlays, performErrorAction, cancelSourceSwitch } = useFrameConnection(props, hasCustomPoster, hasCustomPause);
|
|
259
641
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
260
642
|
play: () => handleRef.current?.play() ?? Promise.resolve(),
|
|
261
|
-
pause
|
|
643
|
+
pause,
|
|
262
644
|
seek: (time) => handleRef.current?.seek(time) ?? Promise.resolve(),
|
|
263
645
|
setMuted: (muted) => handleRef.current?.setMuted(muted) ?? Promise.resolve(),
|
|
264
646
|
setVolume: (volume) => handleRef.current?.setVolume(volume) ?? Promise.resolve(),
|
|
@@ -268,37 +650,191 @@ const VideoPlayerFrame = (0, react.forwardRef)(function VideoPlayerFrame(props,
|
|
|
268
650
|
exitFullscreen: () => handleRef.current?.exitFullscreen() ?? Promise.resolve(),
|
|
269
651
|
setQuality: (level) => handleRef.current?.setQuality(level) ?? Promise.resolve(),
|
|
270
652
|
setSubtitle: (id) => handleRef.current?.setSubtitle(id) ?? Promise.resolve(),
|
|
271
|
-
setLocale: (locale) =>
|
|
653
|
+
setLocale: (locale) => {
|
|
654
|
+
setLocaleOverride(locale);
|
|
655
|
+
return handleRef.current?.setLocale(locale) ?? Promise.resolve();
|
|
656
|
+
},
|
|
272
657
|
pushDanmaku: (item) => handleRef.current?.pushDanmaku(item) ?? Promise.resolve(),
|
|
273
658
|
setDanmakuEnabled: (enabled) => handleRef.current?.setDanmakuEnabled(enabled) ?? Promise.resolve(),
|
|
274
659
|
clearDanmaku: () => handleRef.current?.clearDanmaku() ?? Promise.resolve(),
|
|
275
660
|
retry: () => handleRef.current?.retry() ?? Promise.resolve(),
|
|
276
|
-
|
|
661
|
+
performErrorAction,
|
|
662
|
+
destroy: () => {
|
|
663
|
+
cancelSourceSwitch("destroyed");
|
|
664
|
+
return handleRef.current?.destroy() ?? Promise.resolve();
|
|
665
|
+
},
|
|
277
666
|
getCurrentTime: () => handleRef.current?.getCurrentTime() ?? 0,
|
|
278
667
|
getPlaybackContext: () => handleRef.current?.getPlaybackContext() ?? null,
|
|
279
668
|
getDuration: () => handleRef.current?.getDuration() ?? 0,
|
|
280
669
|
getPlayerHandle: () => handleRef.current
|
|
281
|
-
}), [
|
|
670
|
+
}), [
|
|
671
|
+
handleRef,
|
|
672
|
+
cancelSourceSwitch,
|
|
673
|
+
pause,
|
|
674
|
+
performErrorAction
|
|
675
|
+
]);
|
|
282
676
|
const style = {
|
|
283
677
|
position: "relative",
|
|
678
|
+
display: "block",
|
|
679
|
+
margin: 0,
|
|
284
680
|
width: "100%",
|
|
285
681
|
height: "100%",
|
|
286
|
-
...props.interactive === false ? { pointerEvents: "none" } : {},
|
|
682
|
+
...(props.interactive ?? (props.preset ? _video_lab_protocol.PRESETS[props.preset].interactive : void 0)) === false ? { pointerEvents: "none" } : {},
|
|
287
683
|
...props.style
|
|
288
684
|
};
|
|
685
|
+
const className = ["sentinel-video-frame", props.className].filter(Boolean).join(" ");
|
|
686
|
+
const effectiveLocale = localeOverride ?? props.locale;
|
|
687
|
+
const lang = (0, _video_lab_protocol.normalizeLocaleTag)((typeof effectiveLocale === "string" ? effectiveLocale : effectiveLocale?.locale) || "zh-CN");
|
|
688
|
+
const loadingContext = loadingOverlay && !((loadingOverlay.reason === "initializing" || loadingOverlay.reason === "ok") && !playIntent) ? {
|
|
689
|
+
...loadingOverlay,
|
|
690
|
+
text: (0, _video_lab_protocol.resolveOverlayMessage)((0, _video_lab_protocol.resolveLocaleMessages)(effectiveLocale).messages, `loading.${loadingOverlay.reason}`, lang)
|
|
691
|
+
} : null;
|
|
692
|
+
const { messages } = (0, _video_lab_protocol.resolveLocaleMessages)(effectiveLocale);
|
|
693
|
+
const showHostLoading = loadingContext && !props.renderLoading && props.showDefaultLoadingOverlay !== false && (loadingContext.reason === "frame_disconnected" || hostOwnsOverlays);
|
|
694
|
+
const showHostError = terminalError && props.showErrorOverlay !== false && (terminalError.reason === "frame_disconnected" || terminalError.synthetic === true || hostOwnsOverlays && terminalError.reason === "error");
|
|
695
|
+
const errorActionKey = (0, _video_lab_protocol.errorActionMessageKey)(terminalError ? (0, _video_lab_protocol.effectiveErrorAction)({
|
|
696
|
+
reason: terminalError.reason,
|
|
697
|
+
recoverable: false,
|
|
698
|
+
action: terminalError.action,
|
|
699
|
+
errorCode: terminalError.error.code,
|
|
700
|
+
canResolveSource: Boolean(props.resolveSource),
|
|
701
|
+
canRecreateFrame: true
|
|
702
|
+
}) : "none", terminalError?.error.code);
|
|
289
703
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
290
704
|
ref: containerRef,
|
|
291
|
-
className
|
|
705
|
+
className,
|
|
292
706
|
style,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
707
|
+
lang,
|
|
708
|
+
children: [
|
|
709
|
+
!mounted && (props.children ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
710
|
+
className: "sentinel-video-frame__placeholder",
|
|
711
|
+
style: {
|
|
712
|
+
width: "100%",
|
|
713
|
+
height: "100%",
|
|
714
|
+
background: "#000"
|
|
715
|
+
}
|
|
716
|
+
})),
|
|
717
|
+
!hasCustomPoster && hostPosterVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostPoster, { poster: props.poster }),
|
|
718
|
+
hasCustomPoster && hostPosterVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
719
|
+
"data-sentinel-overlay": "custom-poster",
|
|
720
|
+
style: {
|
|
721
|
+
position: "absolute",
|
|
722
|
+
inset: 0,
|
|
723
|
+
zIndex: 10
|
|
724
|
+
},
|
|
725
|
+
children: props.renderPoster?.()
|
|
726
|
+
}),
|
|
727
|
+
hasCustomPause && hostPauseVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
728
|
+
"data-sentinel-overlay": "custom-pause",
|
|
729
|
+
style: {
|
|
730
|
+
position: "absolute",
|
|
731
|
+
inset: 0,
|
|
732
|
+
zIndex: 13,
|
|
733
|
+
display: "flex",
|
|
734
|
+
alignItems: "center",
|
|
735
|
+
justifyContent: "center"
|
|
736
|
+
},
|
|
737
|
+
children: props.renderPause?.()
|
|
738
|
+
}),
|
|
739
|
+
props.renderLoading && loadingContext && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
740
|
+
className: "sentinel-video-frame__custom-loading",
|
|
741
|
+
"data-sentinel-overlay": "custom-loading",
|
|
742
|
+
style: {
|
|
743
|
+
position: "absolute",
|
|
744
|
+
inset: 0,
|
|
745
|
+
zIndex: 12,
|
|
746
|
+
display: "flex",
|
|
747
|
+
alignItems: "center",
|
|
748
|
+
justifyContent: "center",
|
|
749
|
+
pointerEvents: "none"
|
|
750
|
+
},
|
|
751
|
+
children: props.renderLoading(loadingContext)
|
|
752
|
+
}),
|
|
753
|
+
showHostLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
754
|
+
"data-sentinel-overlay": "loading",
|
|
755
|
+
role: "status",
|
|
756
|
+
"aria-label": loadingContext.text,
|
|
757
|
+
style: {
|
|
758
|
+
position: "absolute",
|
|
759
|
+
inset: 0,
|
|
760
|
+
zIndex: 12,
|
|
761
|
+
display: "flex",
|
|
762
|
+
alignItems: "center",
|
|
763
|
+
justifyContent: "center",
|
|
764
|
+
flexDirection: "column",
|
|
765
|
+
gap: 8,
|
|
766
|
+
color: "#fff",
|
|
767
|
+
background: "rgba(0,0,0,.5)",
|
|
768
|
+
pointerEvents: "none"
|
|
769
|
+
},
|
|
770
|
+
children: [
|
|
771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: "@keyframes sentinel-frame-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.sentinel-frame-spinner{animation:none!important}}" }),
|
|
772
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
773
|
+
className: "sentinel-frame-spinner",
|
|
774
|
+
"aria-hidden": "true",
|
|
775
|
+
style: {
|
|
776
|
+
width: 24,
|
|
777
|
+
height: 24,
|
|
778
|
+
border: "2px solid rgba(255,255,255,.35)",
|
|
779
|
+
borderTopColor: "#fff",
|
|
780
|
+
borderRadius: "50%",
|
|
781
|
+
animation: "sentinel-frame-spin .8s linear infinite"
|
|
782
|
+
}
|
|
783
|
+
}),
|
|
784
|
+
props.showLoadingText === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
785
|
+
part: "text",
|
|
786
|
+
children: loadingContext.text
|
|
787
|
+
})
|
|
788
|
+
]
|
|
789
|
+
}),
|
|
790
|
+
showHostError && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
791
|
+
"data-sentinel-overlay": "error",
|
|
792
|
+
role: "alert",
|
|
793
|
+
style: {
|
|
794
|
+
position: "absolute",
|
|
795
|
+
inset: 0,
|
|
796
|
+
zIndex: 15,
|
|
797
|
+
display: "flex",
|
|
798
|
+
alignItems: "center",
|
|
799
|
+
justifyContent: "center",
|
|
800
|
+
flexDirection: "column",
|
|
801
|
+
gap: 12,
|
|
802
|
+
padding: 16,
|
|
803
|
+
color: "#fff",
|
|
804
|
+
background: "rgba(0,0,0,.8)",
|
|
805
|
+
textAlign: "center"
|
|
806
|
+
},
|
|
807
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
808
|
+
part: "message",
|
|
809
|
+
children: (0, _video_lab_protocol.resolveErrorMessage)(messages, terminalError.error.code, void 0, lang)
|
|
810
|
+
}), errorActionKey && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
811
|
+
type: "button",
|
|
812
|
+
onClick: () => void performErrorAction().catch(() => {}),
|
|
813
|
+
style: {
|
|
814
|
+
border: 0,
|
|
815
|
+
borderRadius: 6,
|
|
816
|
+
padding: "8px 16px",
|
|
817
|
+
background: props.errorButtonColors?.background ?? "#3B82F6",
|
|
818
|
+
color: props.errorButtonColors?.foreground ?? "#fff",
|
|
819
|
+
cursor: "pointer"
|
|
820
|
+
},
|
|
821
|
+
children: (0, _video_lab_protocol.resolveOverlayMessage)(messages, errorActionKey, lang)
|
|
822
|
+
})]
|
|
823
|
+
})
|
|
824
|
+
]
|
|
301
825
|
});
|
|
302
826
|
});
|
|
303
827
|
//#endregion
|
|
828
|
+
Object.defineProperty(exports, "PlayerProvider", {
|
|
829
|
+
enumerable: true,
|
|
830
|
+
get: function() {
|
|
831
|
+
return _video_lab_react_config.PlayerProvider;
|
|
832
|
+
}
|
|
833
|
+
});
|
|
304
834
|
exports.VideoPlayerFrame = VideoPlayerFrame;
|
|
835
|
+
Object.defineProperty(exports, "usePlayerI18n", {
|
|
836
|
+
enumerable: true,
|
|
837
|
+
get: function() {
|
|
838
|
+
return _video_lab_react_config.usePlayerI18n;
|
|
839
|
+
}
|
|
840
|
+
});
|