@video-lab/react 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- }, [rebuildKey]);
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
- setRebuildKey((k) => k + 1);
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
- * **契约声明的 19 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
250
+ * **契约声明的 27 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
236
251
  * 静态断言。曾经漏过 4 个(seeking / seeked / waiting / playing):前两个有注释说
237
252
  * 「团队封装层没用到,需要时再加」,后两个连注释都没有 —— 被 `default: break` 静默吞掉。
238
- * 而静态 iframe 模式(embed-app 裸广播,无过滤)19 个全发,于是同一份契约在四种接入方式
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
- reconnect: (options) => handleRef.current?.reconnect(options),
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.error.retryable,
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
- playable: boolean;
171
- reason: PlayableReason;
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
- reconnect(options?: {
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, PauseImageConfig, PlayableReason, PlaybackContext, PlaybackContextPayload, PlayerError, PlayerEvent, PosterConfig, PresetName, QualityLevel, SourceRouteEventPayload, SubtitleTrackInfo, UserActionPayload } from "@video-lab/protocol";
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
- playable: boolean;
171
- reason: PlayableReason;
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
- reconnect(options?: {
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
- }, [rebuildKey]);
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
- setRebuildKey((k) => k + 1);
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
- * **契约声明的 19 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
249
+ * **契约声明的 27 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
235
250
  * 静态断言。曾经漏过 4 个(seeking / seeked / waiting / playing):前两个有注释说
236
251
  * 「团队封装层没用到,需要时再加」,后两个连注释都没有 —— 被 `default: break` 静默吞掉。
237
- * 而静态 iframe 模式(embed-app 裸广播,无过滤)19 个全发,于是同一份契约在四种接入方式
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
- reconnect: (options) => handleRef.current?.reconnect(options),
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.error.retryable,
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": "2.0.0",
3
+ "version": "3.0.1",
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": "2.0.0",
38
- "@video-lab/player-core": "2.0.0",
39
- "@video-lab/player-ui": "2.0.0"
37
+ "@video-lab/protocol": "3.0.1",
38
+ "@video-lab/player-core": "3.0.1",
39
+ "@video-lab/player-ui": "3.0.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@testing-library/jest-dom": "^6.0.0",