@video-lab/react 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/index.cjs +33 -24
- package/dist/index.d.cts +20 -16
- package/dist/index.d.mts +20 -16
- package/dist/index.mjs +33 -24
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -77,3 +77,35 @@ await ref.current?.enterFullscreen()
|
|
|
77
77
|
- 契约定义:[`@video-lab/protocol`](https://www.npmjs.com/package/@video-lab/protocol)
|
|
78
78
|
|
|
79
79
|
MIT
|
|
80
|
+
|
|
81
|
+
## 错误 UI 接管
|
|
82
|
+
|
|
83
|
+
通过 `showErrorOverlay={false}` 关闭默认错误文字、背景和 Retry。省略时保持开启,错误事件与恢复能力不受影响;宿主自行决定提示文案和样式。本项仅初始化读取,改变时须重新挂载。iframe 需使用 v1.1.0 或更高的兼容应用,旧应用可能仍显示默认错误 UI。纯 iframe 标签需另接 helper 才能订阅事件。
|
|
84
|
+
|
|
85
|
+
## 内置控件按需隐藏
|
|
86
|
+
|
|
87
|
+
初始化 prop `controlVisibility`(Vue 模板写 `:control-visibility`)支持 `{ cssFullscreen: false }` 仅隐藏 CSS 全屏按钮。
|
|
88
|
+
八个可选键为 `play`、`progress`、`time`、`volume`、`playbackRate`、`fullscreen`、`cssFullscreen`、`pip`。
|
|
89
|
+
false 隐藏入口,true/省略保留平台默认;`controls=false` 整体关闭优先。隐藏不禁用既有命令,
|
|
90
|
+
运行时修改须由宿主显式重建,不增加 PiP RPC。
|
|
91
|
+
|
|
92
|
+
## 宿主网页全屏
|
|
93
|
+
|
|
94
|
+
通过 `pageFullscreen` 传入宿主布局适配器(`setActive(active)` / `dispose()`),句柄 `setPageFullscreen(active)` 等待布局确认。实际状态事件为 `pagefullscreenchange`;React 使用 `onPageFullscreenChange`,Vue 使用 `@page-fullscreen-change`。
|
|
95
|
+
|
|
96
|
+
详见[网页全屏接入与完整参考实现](../../docs/guides/PAGE-FULLSCREEN.md)。iframe 需要新版宿主与 iframe 协商支持;未启用时新入口不可用。浏览器原生全屏接口保持独立。
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### 统一恢复(契约 v2)
|
|
100
|
+
|
|
101
|
+
旧 `reconnect({ resetCounter })` 和 `reconnectstart/success/failed` 已移除。命令入口是 `retry(): Promise<void>`;Promise 完成只表示接受或合并请求。恢复中的重复请求共用预算,强播放证据通过 `recovery` 的 `recovered` 回报。
|
|
102
|
+
|
|
103
|
+
宿主 UI 读取 `playablechange` 的 `playable`、`recoverable`、`action`。`error` 只提供诊断;预算耗尽才给出 `action: 'retry'`,宿主无需按错误原因拼接重试状态。使用 `showErrorOverlay={false}` 接管错误样式,使用 `showLoadingOverlay={false}` 接管运行时 Loading;配置在构造时生效,事件仍保留。静态 iframe URL 没有宿主命令通道,内部按钮仍走同一调度。
|
|
104
|
+
|
|
105
|
+
更多迁移语义见 [ADR-099](../../docs/adr/ADR-099-unified-recovery-contract.md)。
|
|
106
|
+
|
|
107
|
+
## 完整事件与命令失败
|
|
108
|
+
|
|
109
|
+
`onPlayerEvent` 保持交付 raw `PlayerEvent`;需要跨重连排序、去重或识别迟到事件时使用
|
|
110
|
+
`onDeliveredPlayerEvent`。所有可失败命令都应 `await` 并 `catch`,并同时订阅事件流:命令
|
|
111
|
+
rejection 不保证产生 `error`,播放期 `error` 也不保证对应某个命令 rejection。
|
package/dist/index.cjs
CHANGED
|
@@ -65,7 +65,6 @@ function usePlayer(props) {
|
|
|
65
65
|
const handleRef = (0, react.useRef)(null);
|
|
66
66
|
const [overlay, setOverlay] = (0, react.useState)(_video_lab_player_ui.INITIAL_OVERLAY);
|
|
67
67
|
const [mounted, setMounted] = (0, react.useState)(false);
|
|
68
|
-
const [rebuildKey, setRebuildKey] = (0, react.useState)(0);
|
|
69
68
|
const propsRef = (0, react.useRef)(props);
|
|
70
69
|
propsRef.current = props;
|
|
71
70
|
/**
|
|
@@ -104,7 +103,13 @@ function usePlayer(props) {
|
|
|
104
103
|
*/
|
|
105
104
|
const reportError = (0, react.useCallback)((err) => {
|
|
106
105
|
const errorEvent = (0, _video_lab_protocol.toErrorEvent)(err);
|
|
107
|
-
setOverlay((prev) => (0, _video_lab_player_ui.reduceOverlay)(prev, errorEvent)
|
|
106
|
+
setOverlay((prev) => handleRef.current ? (0, _video_lab_player_ui.reduceOverlay)(prev, errorEvent) : {
|
|
107
|
+
...prev,
|
|
108
|
+
posterVisible: false,
|
|
109
|
+
loadingVisible: false,
|
|
110
|
+
error: errorEvent.payload,
|
|
111
|
+
action: "replace-source"
|
|
112
|
+
});
|
|
108
113
|
forward(errorEvent, propsRef.current);
|
|
109
114
|
}, []);
|
|
110
115
|
/**
|
|
@@ -139,9 +144,15 @@ function usePlayer(props) {
|
|
|
139
144
|
handleRef.current = (0, _video_lab_player_core.createPlayer)({
|
|
140
145
|
el: container,
|
|
141
146
|
config: toConfig(p),
|
|
147
|
+
pageFullscreen: p.pageFullscreen,
|
|
142
148
|
onEvent: (event) => {
|
|
143
149
|
setOverlay((prev) => (0, _video_lab_player_ui.reduceOverlay)(prev, event));
|
|
144
150
|
forward(event, propsRef.current);
|
|
151
|
+
},
|
|
152
|
+
onDeliveredEvent: (event) => {
|
|
153
|
+
try {
|
|
154
|
+
propsRef.current.onDeliveredPlayerEvent?.(event);
|
|
155
|
+
} catch {}
|
|
145
156
|
}
|
|
146
157
|
});
|
|
147
158
|
setMounted(true);
|
|
@@ -156,7 +167,7 @@ function usePlayer(props) {
|
|
|
156
167
|
mutedInited.current = false;
|
|
157
168
|
localeInited.current = false;
|
|
158
169
|
};
|
|
159
|
-
}, [
|
|
170
|
+
}, []);
|
|
160
171
|
(0, react.useEffect)(() => {
|
|
161
172
|
if (!sourceInited.current) {
|
|
162
173
|
sourceInited.current = true;
|
|
@@ -193,8 +204,9 @@ function usePlayer(props) {
|
|
|
193
204
|
overlay,
|
|
194
205
|
mounted,
|
|
195
206
|
handleRetry: (0, react.useCallback)(() => {
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
const current = handleRef.current;
|
|
208
|
+
if (current) (overlay.action === "play" ? current.play() : current.retry()).catch(() => {});
|
|
209
|
+
}, [overlay.action]),
|
|
198
210
|
dismissPauseImage: (0, react.useCallback)(() => {
|
|
199
211
|
setOverlay((prev) => ({
|
|
200
212
|
...prev,
|
|
@@ -220,6 +232,9 @@ function toConfig(props) {
|
|
|
220
232
|
if (props.startTime !== void 0) config.startTime = props.startTime;
|
|
221
233
|
if (props.preload !== void 0) config.preload = props.preload;
|
|
222
234
|
if (props.controls !== void 0) config.controls = props.controls;
|
|
235
|
+
if (props.showErrorOverlay !== void 0) config.showErrorOverlay = props.showErrorOverlay;
|
|
236
|
+
if (props.showLoadingOverlay !== void 0) config.showLoadingOverlay = props.showLoadingOverlay;
|
|
237
|
+
if (props.controlVisibility !== void 0) config.controlVisibility = props.controlVisibility;
|
|
223
238
|
if (props.interactive !== void 0) config.interactive = props.interactive;
|
|
224
239
|
if (props.poster !== void 0) config.poster = props.poster;
|
|
225
240
|
if (props.pauseImage !== void 0) config.pauseImage = props.pauseImage;
|
|
@@ -232,10 +247,10 @@ function toConfig(props) {
|
|
|
232
247
|
* 播放器事件 → React 回调。名字从 wire 上的小写换成 `onCamelCase`。
|
|
233
248
|
* 和 react-frame / vue-frame 的 forward 一一对应(cross-mode-parity):同样的事件、同样的 payload。
|
|
234
249
|
*
|
|
235
|
-
* **契约声明的
|
|
250
|
+
* **契约声明的 27 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
|
|
236
251
|
* 静态断言。曾经漏过 4 个(seeking / seeked / waiting / playing):前两个有注释说
|
|
237
252
|
* 「团队封装层没用到,需要时再加」,后两个连注释都没有 —— 被 `default: break` 静默吞掉。
|
|
238
|
-
* 而静态 iframe 模式(embed-app 裸广播,无过滤)
|
|
253
|
+
* 而静态 iframe 模式(embed-app 裸广播,无过滤)27 个全发,于是同一份契约在四种接入方式
|
|
239
254
|
* 下能收到的事件不一样,parity 红线实际已破。别再以"业务暂时用不到"为由少接一个。
|
|
240
255
|
*/
|
|
241
256
|
function forward(event, props) {
|
|
@@ -285,18 +300,6 @@ function forward(event, props) {
|
|
|
285
300
|
case "autoplayblocked":
|
|
286
301
|
props.onAutoplayBlocked?.();
|
|
287
302
|
break;
|
|
288
|
-
case "reconnectstart":
|
|
289
|
-
props.onReconnectStart?.({
|
|
290
|
-
attempt: event.payload.attempt,
|
|
291
|
-
maxAttempts: event.payload.maxAttempts
|
|
292
|
-
});
|
|
293
|
-
break;
|
|
294
|
-
case "reconnectsuccess":
|
|
295
|
-
props.onReconnectSuccess?.();
|
|
296
|
-
break;
|
|
297
|
-
case "reconnectfailed":
|
|
298
|
-
props.onReconnectFailed?.();
|
|
299
|
-
break;
|
|
300
303
|
case "compatwarning":
|
|
301
304
|
props.onCompatWarning?.(event.payload);
|
|
302
305
|
break;
|
|
@@ -321,6 +324,9 @@ function forward(event, props) {
|
|
|
321
324
|
case "framefreeze":
|
|
322
325
|
props.onFrameFreeze?.(event.payload);
|
|
323
326
|
break;
|
|
327
|
+
case "pagefullscreenchange":
|
|
328
|
+
props.onPageFullscreenChange?.(event.payload);
|
|
329
|
+
break;
|
|
324
330
|
case "useraction":
|
|
325
331
|
props.onUserAction?.(event.payload);
|
|
326
332
|
break;
|
|
@@ -359,6 +365,8 @@ function forward(event, props) {
|
|
|
359
365
|
* ref.current?.play()
|
|
360
366
|
*/
|
|
361
367
|
const VideoPlayer = (0, react.forwardRef)(function VideoPlayer(props, ref) {
|
|
368
|
+
const [showErrorOverlay] = (0, react.useState)(() => props.showErrorOverlay !== false);
|
|
369
|
+
const [showLoadingOverlay] = (0, react.useState)(() => props.showLoadingOverlay !== false);
|
|
362
370
|
const { containerRef, handleRef, overlay, mounted, handleRetry, dismissPauseImage } = usePlayer(props);
|
|
363
371
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
364
372
|
play: () => handleRef.current?.play() ?? Promise.resolve(),
|
|
@@ -367,6 +375,7 @@ const VideoPlayer = (0, react.forwardRef)(function VideoPlayer(props, ref) {
|
|
|
367
375
|
setMuted: (muted) => handleRef.current?.setMuted(muted),
|
|
368
376
|
setVolume: (volume) => handleRef.current?.setVolume(volume),
|
|
369
377
|
setPlaybackRate: (rate) => handleRef.current?.setPlaybackRate(rate),
|
|
378
|
+
setPageFullscreen: (active) => handleRef.current?.setPageFullscreen(active) ?? Promise.reject(/* @__PURE__ */ new Error("播放器未就绪")),
|
|
370
379
|
enterFullscreen: () => handleRef.current?.enterFullscreen() ?? Promise.resolve(),
|
|
371
380
|
exitFullscreen: () => handleRef.current?.exitFullscreen() ?? Promise.resolve(),
|
|
372
381
|
setQuality: (level) => handleRef.current?.setQuality(level),
|
|
@@ -375,7 +384,7 @@ const VideoPlayer = (0, react.forwardRef)(function VideoPlayer(props, ref) {
|
|
|
375
384
|
pushDanmaku: (item) => handleRef.current?.pushDanmaku(item),
|
|
376
385
|
setDanmakuEnabled: (enabled) => handleRef.current?.setDanmakuEnabled(enabled),
|
|
377
386
|
clearDanmaku: () => handleRef.current?.clearDanmaku(),
|
|
378
|
-
|
|
387
|
+
retry: () => handleRef.current?.retry() ?? Promise.resolve(),
|
|
379
388
|
destroy: () => handleRef.current?.destroy(),
|
|
380
389
|
getCurrentTime: () => handleRef.current?.getCurrentTime() ?? 0,
|
|
381
390
|
getPlaybackContext: () => handleRef.current?.getPlaybackContext() ?? null,
|
|
@@ -417,15 +426,15 @@ const VideoPlayer = (0, react.forwardRef)(function VideoPlayer(props, ref) {
|
|
|
417
426
|
"onsentinel-close": dismissPauseImage
|
|
418
427
|
}),
|
|
419
428
|
overlayElement("sentinel-loading", {
|
|
420
|
-
visible: overlay.loadingVisible,
|
|
429
|
+
visible: showLoadingOverlay && overlay.loadingVisible,
|
|
421
430
|
text: resolveText(messages, "loading")
|
|
422
431
|
}),
|
|
423
|
-
overlay.error && overlayElement("sentinel-error", {
|
|
432
|
+
showErrorOverlay && overlay.error && overlayElement("sentinel-error", {
|
|
424
433
|
visible: true,
|
|
425
434
|
code: overlay.error.code,
|
|
426
435
|
message: resolveErrorText(messages, overlay.error.code, overlay.error.message),
|
|
427
|
-
retryable: overlay.
|
|
428
|
-
"retry-label": resolveText(messages, "retry"),
|
|
436
|
+
retryable: overlay.action === "retry" || overlay.action === "play",
|
|
437
|
+
"retry-label": resolveText(messages, overlay.action === "play" ? "play" : "retry"),
|
|
429
438
|
"onsentinel-retry": handleRetry
|
|
430
439
|
}),
|
|
431
440
|
!mounted && props.children
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { AudioHealthPayload, BufferHealthPayload, ControlVisibilityConfig, DanmakuConfig, DanmakuItem, DeliveredPlayerEvent, FirstFramePayload, FrameFreezePayload, KernelHealthPayload, LocaleConfig, MediaSource, PageFullscreenAdapter, PageFullscreenAdapter as PageFullscreenAdapter$1, PageFullscreenState, PageFullscreenState as PageFullscreenState$1, PauseImageConfig, PlaybackContext, PlaybackContextPayload, PlayerError, PlayerEvent, PosterConfig, PresetName, QualityLevel, SourceRouteEventPayload, SubtitleTrackInfo, UserActionPayload } from "@video-lab/protocol";
|
|
1
2
|
import { PlayerCoreHandle } from "@video-lab/player-core";
|
|
2
|
-
import { AudioHealthPayload, BufferHealthPayload, DanmakuConfig, DanmakuItem, FirstFramePayload, FrameFreezePayload, KernelHealthPayload, LocaleConfig, MediaSource, PauseImageConfig, PlayableReason, PlaybackContext, PlaybackContextPayload, PlayerError, PlayerEvent, PosterConfig, PresetName, QualityLevel, SourceRouteEventPayload, SubtitleTrackInfo, UserActionPayload } from "@video-lab/protocol";
|
|
3
3
|
import { CSSProperties, ReactNode } from "react";
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
/**
|
|
@@ -18,12 +18,20 @@ type PlayerHandle = PlayerCoreHandle;
|
|
|
18
18
|
* payload 形状对齐 protocol 的 PlayerEvent。
|
|
19
19
|
*/
|
|
20
20
|
interface VideoPlayerProps {
|
|
21
|
+
/** 挂载期间保持稳定的宿主网页全屏布局适配器。 */
|
|
22
|
+
pageFullscreen?: PageFullscreenAdapter$1;
|
|
21
23
|
/** 视频源。字符串按 URL 处理,对象可带 type / live / hls 等 */
|
|
22
24
|
source: MediaSource | string;
|
|
23
25
|
autoplay?: boolean;
|
|
24
26
|
muted?: boolean;
|
|
25
27
|
loop?: boolean;
|
|
26
28
|
controls?: boolean;
|
|
29
|
+
/** 默认错误覆盖层,省略时开启。仅初始化读取;变更须重新挂载,不影响错误事件。 */
|
|
30
|
+
showErrorOverlay?: boolean;
|
|
31
|
+
/** 是否显示 SDK 运行时 Loading;构造期读取,默认 true。 */
|
|
32
|
+
showLoadingOverlay?: boolean;
|
|
33
|
+
/** 内置控件初始化显示策略;修改后须由宿主显式重建播放器。 */
|
|
34
|
+
controlVisibility?: ControlVisibilityConfig;
|
|
27
35
|
/** false 时播放器不接收指针事件(点击穿透到下层,如首页预览卡片) */
|
|
28
36
|
interactive?: boolean;
|
|
29
37
|
playsinline?: boolean;
|
|
@@ -88,6 +96,8 @@ interface VideoPlayerProps {
|
|
|
88
96
|
children?: ReactNode;
|
|
89
97
|
/** 完整的契约事件流,供遥测适配器使用;不裁剪 payload。 */
|
|
90
98
|
onPlayerEvent?: (event: PlayerEvent) => void;
|
|
99
|
+
/** 完整投递流;旧 onPlayerEvent 保持 raw PlayerEvent。 */
|
|
100
|
+
onDeliveredPlayerEvent?: (event: DeliveredPlayerEvent) => void;
|
|
91
101
|
/**
|
|
92
102
|
* metadata 就绪。`quality` 是可切档位清单(单码率源为空数组),团队层据此渲染清晰度菜单;
|
|
93
103
|
* `subtitles` 是可切字幕轨清单(无字幕源为空/缺省,契约 1.0.0 起可选,ADR-027),团队层据此渲染字幕菜单。
|
|
@@ -137,12 +147,6 @@ interface VideoPlayerProps {
|
|
|
137
147
|
}) => void;
|
|
138
148
|
onError?: (payload: PlayerError) => void;
|
|
139
149
|
onAutoplayBlocked?: () => void;
|
|
140
|
-
onReconnectStart?: (payload: {
|
|
141
|
-
attempt: number;
|
|
142
|
-
maxAttempts: number;
|
|
143
|
-
}) => void;
|
|
144
|
-
onReconnectSuccess?: () => void;
|
|
145
|
-
onReconnectFailed?: () => void;
|
|
146
150
|
onCompatWarning?: (payload: {
|
|
147
151
|
code: string;
|
|
148
152
|
message: string;
|
|
@@ -166,11 +170,9 @@ interface VideoPlayerProps {
|
|
|
166
170
|
* `recoverable` 决定盖 loading(等)还是露重试入口(不等);`reason='degraded'`
|
|
167
171
|
* 是唯一 `playable` 仍为 `true` 的值(掉帧严重但画面还在动,可以忽略)。
|
|
168
172
|
*/
|
|
169
|
-
onPlayableChange?: (payload: {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
recoverable: boolean;
|
|
173
|
-
}) => void;
|
|
173
|
+
onPlayableChange?: (payload: Extract<PlayerEvent, {
|
|
174
|
+
event: 'playablechange';
|
|
175
|
+
}>['payload']) => void;
|
|
174
176
|
/**
|
|
175
177
|
* 内核健康 —— **非致命内核诊断的聚合**(契约 4.1.0 / ADR-062 / issue #391)。
|
|
176
178
|
*
|
|
@@ -218,6 +220,8 @@ interface VideoPlayerProps {
|
|
|
218
220
|
* ⚠️ 它不是掉帧率,也不与 `stalled` 重复(后者是缓冲驱动的等待)。
|
|
219
221
|
*/
|
|
220
222
|
onFrameFreeze?: (payload: FrameFreezePayload) => void;
|
|
223
|
+
/** 实际网页全屏状态变化。 */
|
|
224
|
+
onPageFullscreenChange?: (payload: PageFullscreenState$1) => void;
|
|
221
225
|
/**
|
|
222
226
|
* 用户动作(ADR-075),经白名单过滤,只含**发生在 SDK 内部**的那些。
|
|
223
227
|
*
|
|
@@ -242,6 +246,8 @@ interface VideoPlayerHandle {
|
|
|
242
246
|
setMuted(muted: boolean): void;
|
|
243
247
|
setVolume(volume: number): void;
|
|
244
248
|
setPlaybackRate(rate: number): void;
|
|
249
|
+
/** 设置网页全屏;必须等宿主布局与 iframe 状态确认。 */
|
|
250
|
+
setPageFullscreen(active: boolean): Promise<void>;
|
|
245
251
|
/** 进入全屏(容器托管,不碰 video 原生入口,iOS 微信崩溃入口由 FullscreenGuard 中和) */
|
|
246
252
|
enterFullscreen(): Promise<void>;
|
|
247
253
|
/** 退出全屏 */
|
|
@@ -257,9 +263,7 @@ interface VideoPlayerHandle {
|
|
|
257
263
|
setDanmakuEnabled(enabled: boolean): void;
|
|
258
264
|
/** 清空屏上弹幕(ADR-028) */
|
|
259
265
|
clearDanmaku(): void;
|
|
260
|
-
|
|
261
|
-
resetCounter?: boolean;
|
|
262
|
-
}): void;
|
|
266
|
+
retry(): Promise<void>;
|
|
263
267
|
destroy(): void;
|
|
264
268
|
getCurrentTime(): number;
|
|
265
269
|
getDuration(): number;
|
|
@@ -306,5 +310,5 @@ interface VideoPlayerHandle {
|
|
|
306
310
|
*/
|
|
307
311
|
declare const VideoPlayer: import("react").ForwardRefExoticComponent<VideoPlayerProps & import("react").RefAttributes<VideoPlayerHandle>>;
|
|
308
312
|
//#endregion
|
|
309
|
-
export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayer, type VideoPlayerHandle, type VideoPlayerProps };
|
|
313
|
+
export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PageFullscreenAdapter, type PageFullscreenState, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayer, type VideoPlayerHandle, type VideoPlayerProps };
|
|
310
314
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CSSProperties, ReactNode } from "react";
|
|
2
|
-
import { AudioHealthPayload, BufferHealthPayload, DanmakuConfig, DanmakuItem, FirstFramePayload, FrameFreezePayload, KernelHealthPayload, LocaleConfig, MediaSource,
|
|
2
|
+
import { AudioHealthPayload, BufferHealthPayload, ControlVisibilityConfig, DanmakuConfig, DanmakuItem, DeliveredPlayerEvent, FirstFramePayload, FrameFreezePayload, KernelHealthPayload, LocaleConfig, MediaSource, PageFullscreenAdapter, PageFullscreenAdapter as PageFullscreenAdapter$1, PageFullscreenState, PageFullscreenState as PageFullscreenState$1, PauseImageConfig, PlaybackContext, PlaybackContextPayload, PlayerError, PlayerEvent, PosterConfig, PresetName, QualityLevel, SourceRouteEventPayload, SubtitleTrackInfo, UserActionPayload } from "@video-lab/protocol";
|
|
3
3
|
import { PlayerCoreHandle } from "@video-lab/player-core";
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
/**
|
|
@@ -18,12 +18,20 @@ type PlayerHandle = PlayerCoreHandle;
|
|
|
18
18
|
* payload 形状对齐 protocol 的 PlayerEvent。
|
|
19
19
|
*/
|
|
20
20
|
interface VideoPlayerProps {
|
|
21
|
+
/** 挂载期间保持稳定的宿主网页全屏布局适配器。 */
|
|
22
|
+
pageFullscreen?: PageFullscreenAdapter$1;
|
|
21
23
|
/** 视频源。字符串按 URL 处理,对象可带 type / live / hls 等 */
|
|
22
24
|
source: MediaSource | string;
|
|
23
25
|
autoplay?: boolean;
|
|
24
26
|
muted?: boolean;
|
|
25
27
|
loop?: boolean;
|
|
26
28
|
controls?: boolean;
|
|
29
|
+
/** 默认错误覆盖层,省略时开启。仅初始化读取;变更须重新挂载,不影响错误事件。 */
|
|
30
|
+
showErrorOverlay?: boolean;
|
|
31
|
+
/** 是否显示 SDK 运行时 Loading;构造期读取,默认 true。 */
|
|
32
|
+
showLoadingOverlay?: boolean;
|
|
33
|
+
/** 内置控件初始化显示策略;修改后须由宿主显式重建播放器。 */
|
|
34
|
+
controlVisibility?: ControlVisibilityConfig;
|
|
27
35
|
/** false 时播放器不接收指针事件(点击穿透到下层,如首页预览卡片) */
|
|
28
36
|
interactive?: boolean;
|
|
29
37
|
playsinline?: boolean;
|
|
@@ -88,6 +96,8 @@ interface VideoPlayerProps {
|
|
|
88
96
|
children?: ReactNode;
|
|
89
97
|
/** 完整的契约事件流,供遥测适配器使用;不裁剪 payload。 */
|
|
90
98
|
onPlayerEvent?: (event: PlayerEvent) => void;
|
|
99
|
+
/** 完整投递流;旧 onPlayerEvent 保持 raw PlayerEvent。 */
|
|
100
|
+
onDeliveredPlayerEvent?: (event: DeliveredPlayerEvent) => void;
|
|
91
101
|
/**
|
|
92
102
|
* metadata 就绪。`quality` 是可切档位清单(单码率源为空数组),团队层据此渲染清晰度菜单;
|
|
93
103
|
* `subtitles` 是可切字幕轨清单(无字幕源为空/缺省,契约 1.0.0 起可选,ADR-027),团队层据此渲染字幕菜单。
|
|
@@ -137,12 +147,6 @@ interface VideoPlayerProps {
|
|
|
137
147
|
}) => void;
|
|
138
148
|
onError?: (payload: PlayerError) => void;
|
|
139
149
|
onAutoplayBlocked?: () => void;
|
|
140
|
-
onReconnectStart?: (payload: {
|
|
141
|
-
attempt: number;
|
|
142
|
-
maxAttempts: number;
|
|
143
|
-
}) => void;
|
|
144
|
-
onReconnectSuccess?: () => void;
|
|
145
|
-
onReconnectFailed?: () => void;
|
|
146
150
|
onCompatWarning?: (payload: {
|
|
147
151
|
code: string;
|
|
148
152
|
message: string;
|
|
@@ -166,11 +170,9 @@ interface VideoPlayerProps {
|
|
|
166
170
|
* `recoverable` 决定盖 loading(等)还是露重试入口(不等);`reason='degraded'`
|
|
167
171
|
* 是唯一 `playable` 仍为 `true` 的值(掉帧严重但画面还在动,可以忽略)。
|
|
168
172
|
*/
|
|
169
|
-
onPlayableChange?: (payload: {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
recoverable: boolean;
|
|
173
|
-
}) => void;
|
|
173
|
+
onPlayableChange?: (payload: Extract<PlayerEvent, {
|
|
174
|
+
event: 'playablechange';
|
|
175
|
+
}>['payload']) => void;
|
|
174
176
|
/**
|
|
175
177
|
* 内核健康 —— **非致命内核诊断的聚合**(契约 4.1.0 / ADR-062 / issue #391)。
|
|
176
178
|
*
|
|
@@ -218,6 +220,8 @@ interface VideoPlayerProps {
|
|
|
218
220
|
* ⚠️ 它不是掉帧率,也不与 `stalled` 重复(后者是缓冲驱动的等待)。
|
|
219
221
|
*/
|
|
220
222
|
onFrameFreeze?: (payload: FrameFreezePayload) => void;
|
|
223
|
+
/** 实际网页全屏状态变化。 */
|
|
224
|
+
onPageFullscreenChange?: (payload: PageFullscreenState$1) => void;
|
|
221
225
|
/**
|
|
222
226
|
* 用户动作(ADR-075),经白名单过滤,只含**发生在 SDK 内部**的那些。
|
|
223
227
|
*
|
|
@@ -242,6 +246,8 @@ interface VideoPlayerHandle {
|
|
|
242
246
|
setMuted(muted: boolean): void;
|
|
243
247
|
setVolume(volume: number): void;
|
|
244
248
|
setPlaybackRate(rate: number): void;
|
|
249
|
+
/** 设置网页全屏;必须等宿主布局与 iframe 状态确认。 */
|
|
250
|
+
setPageFullscreen(active: boolean): Promise<void>;
|
|
245
251
|
/** 进入全屏(容器托管,不碰 video 原生入口,iOS 微信崩溃入口由 FullscreenGuard 中和) */
|
|
246
252
|
enterFullscreen(): Promise<void>;
|
|
247
253
|
/** 退出全屏 */
|
|
@@ -257,9 +263,7 @@ interface VideoPlayerHandle {
|
|
|
257
263
|
setDanmakuEnabled(enabled: boolean): void;
|
|
258
264
|
/** 清空屏上弹幕(ADR-028) */
|
|
259
265
|
clearDanmaku(): void;
|
|
260
|
-
|
|
261
|
-
resetCounter?: boolean;
|
|
262
|
-
}): void;
|
|
266
|
+
retry(): Promise<void>;
|
|
263
267
|
destroy(): void;
|
|
264
268
|
getCurrentTime(): number;
|
|
265
269
|
getDuration(): number;
|
|
@@ -306,5 +310,5 @@ interface VideoPlayerHandle {
|
|
|
306
310
|
*/
|
|
307
311
|
declare const VideoPlayer: import("react").ForwardRefExoticComponent<VideoPlayerProps & import("react").RefAttributes<VideoPlayerHandle>>;
|
|
308
312
|
//#endregion
|
|
309
|
-
export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayer, type VideoPlayerHandle, type VideoPlayerProps };
|
|
313
|
+
export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PageFullscreenAdapter, type PageFullscreenState, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayer, type VideoPlayerHandle, type VideoPlayerProps };
|
|
310
314
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -64,7 +64,6 @@ function usePlayer(props) {
|
|
|
64
64
|
const handleRef = useRef(null);
|
|
65
65
|
const [overlay, setOverlay] = useState(INITIAL_OVERLAY);
|
|
66
66
|
const [mounted, setMounted] = useState(false);
|
|
67
|
-
const [rebuildKey, setRebuildKey] = useState(0);
|
|
68
67
|
const propsRef = useRef(props);
|
|
69
68
|
propsRef.current = props;
|
|
70
69
|
/**
|
|
@@ -103,7 +102,13 @@ function usePlayer(props) {
|
|
|
103
102
|
*/
|
|
104
103
|
const reportError = useCallback((err) => {
|
|
105
104
|
const errorEvent = toErrorEvent(err);
|
|
106
|
-
setOverlay((prev) => reduceOverlay(prev, errorEvent)
|
|
105
|
+
setOverlay((prev) => handleRef.current ? reduceOverlay(prev, errorEvent) : {
|
|
106
|
+
...prev,
|
|
107
|
+
posterVisible: false,
|
|
108
|
+
loadingVisible: false,
|
|
109
|
+
error: errorEvent.payload,
|
|
110
|
+
action: "replace-source"
|
|
111
|
+
});
|
|
107
112
|
forward(errorEvent, propsRef.current);
|
|
108
113
|
}, []);
|
|
109
114
|
/**
|
|
@@ -138,9 +143,15 @@ function usePlayer(props) {
|
|
|
138
143
|
handleRef.current = createPlayer({
|
|
139
144
|
el: container,
|
|
140
145
|
config: toConfig(p),
|
|
146
|
+
pageFullscreen: p.pageFullscreen,
|
|
141
147
|
onEvent: (event) => {
|
|
142
148
|
setOverlay((prev) => reduceOverlay(prev, event));
|
|
143
149
|
forward(event, propsRef.current);
|
|
150
|
+
},
|
|
151
|
+
onDeliveredEvent: (event) => {
|
|
152
|
+
try {
|
|
153
|
+
propsRef.current.onDeliveredPlayerEvent?.(event);
|
|
154
|
+
} catch {}
|
|
144
155
|
}
|
|
145
156
|
});
|
|
146
157
|
setMounted(true);
|
|
@@ -155,7 +166,7 @@ function usePlayer(props) {
|
|
|
155
166
|
mutedInited.current = false;
|
|
156
167
|
localeInited.current = false;
|
|
157
168
|
};
|
|
158
|
-
}, [
|
|
169
|
+
}, []);
|
|
159
170
|
useEffect(() => {
|
|
160
171
|
if (!sourceInited.current) {
|
|
161
172
|
sourceInited.current = true;
|
|
@@ -192,8 +203,9 @@ function usePlayer(props) {
|
|
|
192
203
|
overlay,
|
|
193
204
|
mounted,
|
|
194
205
|
handleRetry: useCallback(() => {
|
|
195
|
-
|
|
196
|
-
|
|
206
|
+
const current = handleRef.current;
|
|
207
|
+
if (current) (overlay.action === "play" ? current.play() : current.retry()).catch(() => {});
|
|
208
|
+
}, [overlay.action]),
|
|
197
209
|
dismissPauseImage: useCallback(() => {
|
|
198
210
|
setOverlay((prev) => ({
|
|
199
211
|
...prev,
|
|
@@ -219,6 +231,9 @@ function toConfig(props) {
|
|
|
219
231
|
if (props.startTime !== void 0) config.startTime = props.startTime;
|
|
220
232
|
if (props.preload !== void 0) config.preload = props.preload;
|
|
221
233
|
if (props.controls !== void 0) config.controls = props.controls;
|
|
234
|
+
if (props.showErrorOverlay !== void 0) config.showErrorOverlay = props.showErrorOverlay;
|
|
235
|
+
if (props.showLoadingOverlay !== void 0) config.showLoadingOverlay = props.showLoadingOverlay;
|
|
236
|
+
if (props.controlVisibility !== void 0) config.controlVisibility = props.controlVisibility;
|
|
222
237
|
if (props.interactive !== void 0) config.interactive = props.interactive;
|
|
223
238
|
if (props.poster !== void 0) config.poster = props.poster;
|
|
224
239
|
if (props.pauseImage !== void 0) config.pauseImage = props.pauseImage;
|
|
@@ -231,10 +246,10 @@ function toConfig(props) {
|
|
|
231
246
|
* 播放器事件 → React 回调。名字从 wire 上的小写换成 `onCamelCase`。
|
|
232
247
|
* 和 react-frame / vue-frame 的 forward 一一对应(cross-mode-parity):同样的事件、同样的 payload。
|
|
233
248
|
*
|
|
234
|
-
* **契约声明的
|
|
249
|
+
* **契约声明的 27 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
|
|
235
250
|
* 静态断言。曾经漏过 4 个(seeking / seeked / waiting / playing):前两个有注释说
|
|
236
251
|
* 「团队封装层没用到,需要时再加」,后两个连注释都没有 —— 被 `default: break` 静默吞掉。
|
|
237
|
-
* 而静态 iframe 模式(embed-app 裸广播,无过滤)
|
|
252
|
+
* 而静态 iframe 模式(embed-app 裸广播,无过滤)27 个全发,于是同一份契约在四种接入方式
|
|
238
253
|
* 下能收到的事件不一样,parity 红线实际已破。别再以"业务暂时用不到"为由少接一个。
|
|
239
254
|
*/
|
|
240
255
|
function forward(event, props) {
|
|
@@ -284,18 +299,6 @@ function forward(event, props) {
|
|
|
284
299
|
case "autoplayblocked":
|
|
285
300
|
props.onAutoplayBlocked?.();
|
|
286
301
|
break;
|
|
287
|
-
case "reconnectstart":
|
|
288
|
-
props.onReconnectStart?.({
|
|
289
|
-
attempt: event.payload.attempt,
|
|
290
|
-
maxAttempts: event.payload.maxAttempts
|
|
291
|
-
});
|
|
292
|
-
break;
|
|
293
|
-
case "reconnectsuccess":
|
|
294
|
-
props.onReconnectSuccess?.();
|
|
295
|
-
break;
|
|
296
|
-
case "reconnectfailed":
|
|
297
|
-
props.onReconnectFailed?.();
|
|
298
|
-
break;
|
|
299
302
|
case "compatwarning":
|
|
300
303
|
props.onCompatWarning?.(event.payload);
|
|
301
304
|
break;
|
|
@@ -320,6 +323,9 @@ function forward(event, props) {
|
|
|
320
323
|
case "framefreeze":
|
|
321
324
|
props.onFrameFreeze?.(event.payload);
|
|
322
325
|
break;
|
|
326
|
+
case "pagefullscreenchange":
|
|
327
|
+
props.onPageFullscreenChange?.(event.payload);
|
|
328
|
+
break;
|
|
323
329
|
case "useraction":
|
|
324
330
|
props.onUserAction?.(event.payload);
|
|
325
331
|
break;
|
|
@@ -358,6 +364,8 @@ function forward(event, props) {
|
|
|
358
364
|
* ref.current?.play()
|
|
359
365
|
*/
|
|
360
366
|
const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
|
|
367
|
+
const [showErrorOverlay] = useState(() => props.showErrorOverlay !== false);
|
|
368
|
+
const [showLoadingOverlay] = useState(() => props.showLoadingOverlay !== false);
|
|
361
369
|
const { containerRef, handleRef, overlay, mounted, handleRetry, dismissPauseImage } = usePlayer(props);
|
|
362
370
|
useImperativeHandle(ref, () => ({
|
|
363
371
|
play: () => handleRef.current?.play() ?? Promise.resolve(),
|
|
@@ -366,6 +374,7 @@ const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
|
|
|
366
374
|
setMuted: (muted) => handleRef.current?.setMuted(muted),
|
|
367
375
|
setVolume: (volume) => handleRef.current?.setVolume(volume),
|
|
368
376
|
setPlaybackRate: (rate) => handleRef.current?.setPlaybackRate(rate),
|
|
377
|
+
setPageFullscreen: (active) => handleRef.current?.setPageFullscreen(active) ?? Promise.reject(/* @__PURE__ */ new Error("播放器未就绪")),
|
|
369
378
|
enterFullscreen: () => handleRef.current?.enterFullscreen() ?? Promise.resolve(),
|
|
370
379
|
exitFullscreen: () => handleRef.current?.exitFullscreen() ?? Promise.resolve(),
|
|
371
380
|
setQuality: (level) => handleRef.current?.setQuality(level),
|
|
@@ -374,7 +383,7 @@ const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
|
|
|
374
383
|
pushDanmaku: (item) => handleRef.current?.pushDanmaku(item),
|
|
375
384
|
setDanmakuEnabled: (enabled) => handleRef.current?.setDanmakuEnabled(enabled),
|
|
376
385
|
clearDanmaku: () => handleRef.current?.clearDanmaku(),
|
|
377
|
-
|
|
386
|
+
retry: () => handleRef.current?.retry() ?? Promise.resolve(),
|
|
378
387
|
destroy: () => handleRef.current?.destroy(),
|
|
379
388
|
getCurrentTime: () => handleRef.current?.getCurrentTime() ?? 0,
|
|
380
389
|
getPlaybackContext: () => handleRef.current?.getPlaybackContext() ?? null,
|
|
@@ -416,15 +425,15 @@ const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
|
|
|
416
425
|
"onsentinel-close": dismissPauseImage
|
|
417
426
|
}),
|
|
418
427
|
overlayElement("sentinel-loading", {
|
|
419
|
-
visible: overlay.loadingVisible,
|
|
428
|
+
visible: showLoadingOverlay && overlay.loadingVisible,
|
|
420
429
|
text: resolveText(messages, "loading")
|
|
421
430
|
}),
|
|
422
|
-
overlay.error && overlayElement("sentinel-error", {
|
|
431
|
+
showErrorOverlay && overlay.error && overlayElement("sentinel-error", {
|
|
423
432
|
visible: true,
|
|
424
433
|
code: overlay.error.code,
|
|
425
434
|
message: resolveErrorText(messages, overlay.error.code, overlay.error.message),
|
|
426
|
-
retryable: overlay.
|
|
427
|
-
"retry-label": resolveText(messages, "retry"),
|
|
435
|
+
retryable: overlay.action === "retry" || overlay.action === "play",
|
|
436
|
+
"retry-label": resolveText(messages, overlay.action === "play" ? "play" : "retry"),
|
|
428
437
|
"onsentinel-retry": handleRetry
|
|
429
438
|
}),
|
|
430
439
|
!mounted && props.children
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@video-lab/react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"react-dom": ">=19.0.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@video-lab/protocol": "
|
|
38
|
-
"@video-lab/player-core": "
|
|
39
|
-
"@video-lab/player-ui": "
|
|
37
|
+
"@video-lab/protocol": "3.0.0",
|
|
38
|
+
"@video-lab/player-core": "3.0.0",
|
|
39
|
+
"@video-lab/player-ui": "3.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@testing-library/jest-dom": "^6.0.0",
|