@video-lab/react-frame 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,6 +77,12 @@ import { VideoPlayerFrame } from '@video-lab/react-frame'
77
77
  - **不内置品牌 UI;认证只支持签名 URL**,不接受自定义请求头。
78
78
  - 业务应用可直接使用本组件,也可在自己的业务组件中封装它。
79
79
 
80
+ ## AI 接入 Skill
81
+
82
+ 安装包包含 `skills/host-integration/SKILL.md`。让宿主 AI 使用前,先确认该文件 front matter
83
+ 中的 `supportedPackages` 与已安装版本匹配;它提供接入面选择、错误/恢复和 delivered 事件的边界,
84
+ 但具体 API 仍以本 README 与类型定义为准。
85
+
80
86
  ## 相关
81
87
 
82
88
  - 完整 props / 事件 / 句柄清单请联系项目维护团队获取内部使用手册。
@@ -84,6 +90,39 @@ import { VideoPlayerFrame } from '@video-lab/react-frame'
84
90
 
85
91
  MIT
86
92
 
93
+ ## 完整事件与命令失败
94
+
95
+ `onPlayerEvent` 保持交付 raw `PlayerEvent`;需要跨重连排序、去重或识别迟到事件时使用
96
+ `onDeliveredPlayerEvent`。所有可失败命令都应 `await` 并 `catch`,并同时订阅事件流:命令
97
+ rejection 不保证产生 `error`,播放期 `error` 也不保证对应某个命令 rejection。
98
+
87
99
  ### iframe 基础路径
88
100
 
89
101
  `origin` 是宿主提供的完整部署基础 URL,可包含 `/custom/player` 等目录。SDK 只追加 `/v{iframe应用版本}/`,不会自动追加 `/embed`。旧部署仍在 `/embed/v…/` 时,请把 `/embed` 显式保留在 `origin` 中;版本目录不要写进 `origin`。
102
+
103
+ ## 错误 UI 接管
104
+
105
+ 通过 `showErrorOverlay={false}` 关闭默认错误文字、背景和 Retry。省略时保持开启,错误事件与恢复能力不受影响;宿主自行决定提示文案和样式。本项仅初始化读取,改变时须重新挂载。iframe 需使用 v1.1.0 或更高的兼容应用,旧应用可能仍显示默认错误 UI。纯 iframe 标签需另接 helper 才能订阅事件。
106
+
107
+ ## 内置控件按需隐藏
108
+
109
+ 初始化 prop `controlVisibility`(Vue 模板写 `:control-visibility`)支持 `{ cssFullscreen: false }` 仅隐藏 CSS 全屏按钮。
110
+ 八个可选键为 `play`、`progress`、`time`、`volume`、`playbackRate`、`fullscreen`、`cssFullscreen`、`pip`。
111
+ false 隐藏入口,true/省略保留平台默认;`controls=false` 整体关闭优先。隐藏不禁用既有命令,
112
+ 运行时修改须由宿主显式重建,不增加 PiP RPC。
113
+ iframe 须使用支持该功能、已部署的精确版本 `/v1.2.0/`;旧 iframe 可能忽略新配置。
114
+
115
+ ## 宿主网页全屏
116
+
117
+ 通过 `pageFullscreen` 传入宿主布局适配器(`setActive(active)` / `dispose()`),句柄 `setPageFullscreen(active)` 等待布局确认。实际状态事件为 `pagefullscreenchange`;React 使用 `onPageFullscreenChange`,Vue 使用 `@page-fullscreen-change`。
118
+
119
+ 详见[网页全屏接入与完整参考实现](../../docs/guides/PAGE-FULLSCREEN.md)。iframe 需要新版宿主与 iframe 协商支持;未启用时新入口不可用。浏览器原生全屏接口保持独立。
120
+
121
+
122
+ ### 统一恢复(契约 v2)
123
+
124
+ 旧 `reconnect({ resetCounter })` 和 `reconnectstart/success/failed` 已移除。命令入口是 `retry(): Promise<void>`;Promise 完成只表示接受或合并请求。恢复中的重复请求共用预算,强播放证据通过 `recovery` 的 `recovered` 回报。
125
+
126
+ 宿主 UI 读取 `playablechange` 的 `playable`、`recoverable`、`action`。`error` 只提供诊断;预算耗尽才给出 `action: 'retry'`,宿主无需按错误原因拼接重试状态。使用 `showErrorOverlay={false}` 接管错误样式,使用 `showLoadingOverlay={false}` 接管运行时 Loading;配置在构造时生效,事件仍保留。静态 iframe URL 没有宿主命令通道,内部按钮仍走同一调度。
127
+
128
+ 更多迁移语义见 [ADR-099](../../docs/adr/ADR-099-unified-recovery-contract.md)。
package/dist/index.cjs CHANGED
@@ -29,12 +29,18 @@ function useFrameConnection(props) {
29
29
  container,
30
30
  source: p.source,
31
31
  config: toConfig(p),
32
+ pageFullscreen: p.pageFullscreen,
32
33
  ...p.origin !== void 0 ? { origin: p.origin } : {},
33
34
  ...p.version !== void 0 ? { version: p.version } : {},
34
35
  onEvent: (event) => {
35
36
  if (event.event === "play" || event.event === "error") setHostPosterVisible(false);
36
37
  forward(event, propsRef.current);
37
38
  },
39
+ onDeliveredEvent: (event) => {
40
+ try {
41
+ propsRef.current.onDeliveredPlayerEvent?.(event);
42
+ } catch {}
43
+ },
38
44
  debug: p.debug ?? false
39
45
  }).then((h) => {
40
46
  if (cancelled) {
@@ -95,6 +101,9 @@ function toConfig(props) {
95
101
  if (props.startTime !== void 0) config.startTime = props.startTime;
96
102
  if (props.preload !== void 0) config.preload = props.preload;
97
103
  if (props.controls !== void 0) config.controls = props.controls;
104
+ if (props.showErrorOverlay !== void 0) config.showErrorOverlay = props.showErrorOverlay;
105
+ if (props.showLoadingOverlay !== void 0) config.showLoadingOverlay = props.showLoadingOverlay;
106
+ if (props.controlVisibility !== void 0) config.controlVisibility = props.controlVisibility;
98
107
  if (props.interactive !== void 0) config.interactive = props.interactive;
99
108
  if (props.poster !== void 0) config.poster = props.poster;
100
109
  if (props.pauseImage !== void 0) config.pauseImage = props.pauseImage;
@@ -107,9 +116,9 @@ function toConfig(props) {
107
116
  * iframe 事件 → React 回调。名字从 wire 上的小写拼写换成 `onCamelCase`。
108
117
  * 和 vue-frame 的 forward 一一对应(cross-mode-parity):同样的事件、同样的 payload 裁剪。
109
118
  *
110
- * **契约声明的 19 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
119
+ * **契约声明的 27 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
111
120
  * 静态断言。曾经漏过 seeking / seeked / waiting / playing 四个,理由是"团队封装层没用到" ——
112
- * 但静态 iframe 模式是裸广播、19 个全发,于是同一份契约在四种接入方式下能收到的事件
121
+ * 但静态 iframe 模式是裸广播、27 个全发,于是同一份契约在四种接入方式下能收到的事件
113
122
  * 不一样,parity 红线实际已破。别再以"业务暂时用不到"为由少接一个。
114
123
  */
115
124
  function forward(event, props) {
@@ -159,18 +168,6 @@ function forward(event, props) {
159
168
  case "autoplayblocked":
160
169
  props.onAutoplayBlocked?.();
161
170
  break;
162
- case "reconnectstart":
163
- props.onReconnectStart?.({
164
- attempt: event.payload.attempt,
165
- maxAttempts: event.payload.maxAttempts
166
- });
167
- break;
168
- case "reconnectsuccess":
169
- props.onReconnectSuccess?.();
170
- break;
171
- case "reconnectfailed":
172
- props.onReconnectFailed?.();
173
- break;
174
171
  case "compatwarning":
175
172
  props.onCompatWarning?.(event.payload);
176
173
  break;
@@ -195,6 +192,9 @@ function forward(event, props) {
195
192
  case "framefreeze":
196
193
  props.onFrameFreeze?.(event.payload);
197
194
  break;
195
+ case "pagefullscreenchange":
196
+ props.onPageFullscreenChange?.(event.payload);
197
+ break;
198
198
  case "useraction":
199
199
  props.onUserAction?.(event.payload);
200
200
  break;
@@ -263,6 +263,7 @@ const VideoPlayerFrame = (0, react.forwardRef)(function VideoPlayerFrame(props,
263
263
  setMuted: (muted) => handleRef.current?.setMuted(muted) ?? Promise.resolve(),
264
264
  setVolume: (volume) => handleRef.current?.setVolume(volume) ?? Promise.resolve(),
265
265
  setPlaybackRate: (rate) => handleRef.current?.setPlaybackRate(rate) ?? Promise.resolve(),
266
+ setPageFullscreen: (active) => handleRef.current?.setPageFullscreen(active) ?? Promise.reject(/* @__PURE__ */ new Error("播放器未就绪")),
266
267
  enterFullscreen: () => handleRef.current?.enterFullscreen() ?? Promise.resolve(),
267
268
  exitFullscreen: () => handleRef.current?.exitFullscreen() ?? Promise.resolve(),
268
269
  setQuality: (level) => handleRef.current?.setQuality(level) ?? Promise.resolve(),
@@ -271,7 +272,7 @@ const VideoPlayerFrame = (0, react.forwardRef)(function VideoPlayerFrame(props,
271
272
  pushDanmaku: (item) => handleRef.current?.pushDanmaku(item) ?? Promise.resolve(),
272
273
  setDanmakuEnabled: (enabled) => handleRef.current?.setDanmakuEnabled(enabled) ?? Promise.resolve(),
273
274
  clearDanmaku: () => handleRef.current?.clearDanmaku() ?? Promise.resolve(),
274
- reconnect: (options) => handleRef.current?.reconnect(options) ?? Promise.resolve(),
275
+ retry: () => handleRef.current?.retry() ?? Promise.resolve(),
275
276
  destroy: () => handleRef.current?.destroy() ?? Promise.resolve(),
276
277
  getCurrentTime: () => handleRef.current?.getCurrentTime() ?? 0,
277
278
  getPlaybackContext: () => handleRef.current?.getPlaybackContext() ?? null,
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 { PlayerFrameHandle } from "@video-lab/frame-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 = PlayerFrameHandle;
18
18
  * protocol 的 PlayerEvent —— 和 vue-frame 的 emits 一一对应(cross-mode-parity)。
19
19
  */
20
20
  interface VideoPlayerFrameProps {
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;
@@ -78,6 +86,7 @@ interface VideoPlayerFrameProps {
78
86
  children?: ReactNode;
79
87
  /** 完整的契约事件流,供遥测适配器使用;不裁剪 iframe 转发的 payload。 */
80
88
  onPlayerEvent?: (event: PlayerEvent) => void;
89
+ onDeliveredPlayerEvent?: (event: DeliveredPlayerEvent) => void;
81
90
  /**
82
91
  * metadata 就绪。`quality` 是可切档位清单(单码率源为空数组),团队层据此渲染清晰度菜单;
83
92
  * `subtitles` 是可切字幕轨清单(无字幕源为空/缺省,契约 1.0.0 起可选,ADR-027),团队层据此渲染字幕菜单。
@@ -127,12 +136,6 @@ interface VideoPlayerFrameProps {
127
136
  }) => void;
128
137
  onError?: (payload: PlayerError) => void;
129
138
  onAutoplayBlocked?: () => void;
130
- onReconnectStart?: (payload: {
131
- attempt: number;
132
- maxAttempts: number;
133
- }) => void;
134
- onReconnectSuccess?: () => void;
135
- onReconnectFailed?: () => void;
136
139
  onCompatWarning?: (payload: {
137
140
  code: string;
138
141
  message: string;
@@ -156,11 +159,9 @@ interface VideoPlayerFrameProps {
156
159
  * `recoverable` 决定盖 loading(等)还是露重试入口(不等);`reason='degraded'`
157
160
  * 是唯一 `playable` 仍为 `true` 的值(掉帧严重但画面还在动,可以忽略)。
158
161
  */
159
- onPlayableChange?: (payload: {
160
- playable: boolean;
161
- reason: PlayableReason;
162
- recoverable: boolean;
163
- }) => void;
162
+ onPlayableChange?: (payload: Extract<PlayerEvent, {
163
+ event: 'playablechange';
164
+ }>['payload']) => void;
164
165
  /**
165
166
  * 内核健康 —— **非致命内核诊断的聚合**(契约 4.1.0 / ADR-062 / issue #391)。
166
167
  *
@@ -203,6 +204,8 @@ interface VideoPlayerFrameProps {
203
204
  * ⚠️ 它不是掉帧率,也不与 `stalled` 重复(后者是缓冲驱动的等待)。
204
205
  */
205
206
  onFrameFreeze?: (payload: FrameFreezePayload) => void;
207
+ /** 实际网页全屏状态变化。 */
208
+ onPageFullscreenChange?: (payload: PageFullscreenState$1) => void;
206
209
  /**
207
210
  * 用户动作(ADR-075),经白名单过滤,只含**发生在 SDK 内部**的那些。
208
211
  *
@@ -225,6 +228,8 @@ interface VideoPlayerFrameHandle {
225
228
  setMuted(muted: boolean): Promise<void>;
226
229
  setVolume(volume: number): Promise<void>;
227
230
  setPlaybackRate(rate: number): Promise<void>;
231
+ /** 设置网页全屏;必须等宿主布局与 iframe 状态确认。 */
232
+ setPageFullscreen(active: boolean): Promise<void>;
228
233
  /** 进入全屏(容器托管,不碰 video 原生入口,iOS 微信崩溃入口由 FullscreenGuard 中和) */
229
234
  enterFullscreen(): Promise<void>;
230
235
  /** 退出全屏 */
@@ -240,9 +245,7 @@ interface VideoPlayerFrameHandle {
240
245
  setDanmakuEnabled(enabled: boolean): Promise<void>;
241
246
  /** 清空屏上弹幕(ADR-028) */
242
247
  clearDanmaku(): Promise<void>;
243
- reconnect(options?: {
244
- resetCounter?: boolean;
245
- }): Promise<void>;
248
+ retry(): Promise<void>;
246
249
  destroy(): Promise<void>;
247
250
  getCurrentTime(): number;
248
251
  getDuration(): number;
@@ -280,5 +283,5 @@ interface VideoPlayerFrameHandle {
280
283
  */
281
284
  declare const VideoPlayerFrame: import("react").ForwardRefExoticComponent<VideoPlayerFrameProps & import("react").RefAttributes<VideoPlayerFrameHandle>>;
282
285
  //#endregion
283
- export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayerFrame, type VideoPlayerFrameHandle, type VideoPlayerFrameProps };
286
+ export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PageFullscreenAdapter, type PageFullscreenState, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayerFrame, type VideoPlayerFrameHandle, type VideoPlayerFrameProps };
284
287
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CSSProperties, ReactNode } from "react";
2
2
  import { PlayerFrameHandle } from "@video-lab/frame-core";
3
- 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
+ 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";
4
4
  //#region src/types.d.ts
5
5
  /**
6
6
  * 消费方拿到的播放器句柄(逃生舱)。
@@ -18,12 +18,20 @@ type PlayerHandle = PlayerFrameHandle;
18
18
  * protocol 的 PlayerEvent —— 和 vue-frame 的 emits 一一对应(cross-mode-parity)。
19
19
  */
20
20
  interface VideoPlayerFrameProps {
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;
@@ -78,6 +86,7 @@ interface VideoPlayerFrameProps {
78
86
  children?: ReactNode;
79
87
  /** 完整的契约事件流,供遥测适配器使用;不裁剪 iframe 转发的 payload。 */
80
88
  onPlayerEvent?: (event: PlayerEvent) => void;
89
+ onDeliveredPlayerEvent?: (event: DeliveredPlayerEvent) => void;
81
90
  /**
82
91
  * metadata 就绪。`quality` 是可切档位清单(单码率源为空数组),团队层据此渲染清晰度菜单;
83
92
  * `subtitles` 是可切字幕轨清单(无字幕源为空/缺省,契约 1.0.0 起可选,ADR-027),团队层据此渲染字幕菜单。
@@ -127,12 +136,6 @@ interface VideoPlayerFrameProps {
127
136
  }) => void;
128
137
  onError?: (payload: PlayerError) => void;
129
138
  onAutoplayBlocked?: () => void;
130
- onReconnectStart?: (payload: {
131
- attempt: number;
132
- maxAttempts: number;
133
- }) => void;
134
- onReconnectSuccess?: () => void;
135
- onReconnectFailed?: () => void;
136
139
  onCompatWarning?: (payload: {
137
140
  code: string;
138
141
  message: string;
@@ -156,11 +159,9 @@ interface VideoPlayerFrameProps {
156
159
  * `recoverable` 决定盖 loading(等)还是露重试入口(不等);`reason='degraded'`
157
160
  * 是唯一 `playable` 仍为 `true` 的值(掉帧严重但画面还在动,可以忽略)。
158
161
  */
159
- onPlayableChange?: (payload: {
160
- playable: boolean;
161
- reason: PlayableReason;
162
- recoverable: boolean;
163
- }) => void;
162
+ onPlayableChange?: (payload: Extract<PlayerEvent, {
163
+ event: 'playablechange';
164
+ }>['payload']) => void;
164
165
  /**
165
166
  * 内核健康 —— **非致命内核诊断的聚合**(契约 4.1.0 / ADR-062 / issue #391)。
166
167
  *
@@ -203,6 +204,8 @@ interface VideoPlayerFrameProps {
203
204
  * ⚠️ 它不是掉帧率,也不与 `stalled` 重复(后者是缓冲驱动的等待)。
204
205
  */
205
206
  onFrameFreeze?: (payload: FrameFreezePayload) => void;
207
+ /** 实际网页全屏状态变化。 */
208
+ onPageFullscreenChange?: (payload: PageFullscreenState$1) => void;
206
209
  /**
207
210
  * 用户动作(ADR-075),经白名单过滤,只含**发生在 SDK 内部**的那些。
208
211
  *
@@ -225,6 +228,8 @@ interface VideoPlayerFrameHandle {
225
228
  setMuted(muted: boolean): Promise<void>;
226
229
  setVolume(volume: number): Promise<void>;
227
230
  setPlaybackRate(rate: number): Promise<void>;
231
+ /** 设置网页全屏;必须等宿主布局与 iframe 状态确认。 */
232
+ setPageFullscreen(active: boolean): Promise<void>;
228
233
  /** 进入全屏(容器托管,不碰 video 原生入口,iOS 微信崩溃入口由 FullscreenGuard 中和) */
229
234
  enterFullscreen(): Promise<void>;
230
235
  /** 退出全屏 */
@@ -240,9 +245,7 @@ interface VideoPlayerFrameHandle {
240
245
  setDanmakuEnabled(enabled: boolean): Promise<void>;
241
246
  /** 清空屏上弹幕(ADR-028) */
242
247
  clearDanmaku(): Promise<void>;
243
- reconnect(options?: {
244
- resetCounter?: boolean;
245
- }): Promise<void>;
248
+ retry(): Promise<void>;
246
249
  destroy(): Promise<void>;
247
250
  getCurrentTime(): number;
248
251
  getDuration(): number;
@@ -280,5 +283,5 @@ interface VideoPlayerFrameHandle {
280
283
  */
281
284
  declare const VideoPlayerFrame: import("react").ForwardRefExoticComponent<VideoPlayerFrameProps & import("react").RefAttributes<VideoPlayerFrameHandle>>;
282
285
  //#endregion
283
- export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayerFrame, type VideoPlayerFrameHandle, type VideoPlayerFrameProps };
286
+ export { type DanmakuConfig, type DanmakuItem, type MediaSource, type PageFullscreenAdapter, type PageFullscreenState, type PlayerError, type PlayerHandle, type QualityLevel, type SubtitleTrackInfo, VideoPlayerFrame, type VideoPlayerFrameHandle, type VideoPlayerFrameProps };
284
287
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -28,12 +28,18 @@ function useFrameConnection(props) {
28
28
  container,
29
29
  source: p.source,
30
30
  config: toConfig(p),
31
+ pageFullscreen: p.pageFullscreen,
31
32
  ...p.origin !== void 0 ? { origin: p.origin } : {},
32
33
  ...p.version !== void 0 ? { version: p.version } : {},
33
34
  onEvent: (event) => {
34
35
  if (event.event === "play" || event.event === "error") setHostPosterVisible(false);
35
36
  forward(event, propsRef.current);
36
37
  },
38
+ onDeliveredEvent: (event) => {
39
+ try {
40
+ propsRef.current.onDeliveredPlayerEvent?.(event);
41
+ } catch {}
42
+ },
37
43
  debug: p.debug ?? false
38
44
  }).then((h) => {
39
45
  if (cancelled) {
@@ -94,6 +100,9 @@ function toConfig(props) {
94
100
  if (props.startTime !== void 0) config.startTime = props.startTime;
95
101
  if (props.preload !== void 0) config.preload = props.preload;
96
102
  if (props.controls !== void 0) config.controls = props.controls;
103
+ if (props.showErrorOverlay !== void 0) config.showErrorOverlay = props.showErrorOverlay;
104
+ if (props.showLoadingOverlay !== void 0) config.showLoadingOverlay = props.showLoadingOverlay;
105
+ if (props.controlVisibility !== void 0) config.controlVisibility = props.controlVisibility;
97
106
  if (props.interactive !== void 0) config.interactive = props.interactive;
98
107
  if (props.poster !== void 0) config.poster = props.poster;
99
108
  if (props.pauseImage !== void 0) config.pauseImage = props.pauseImage;
@@ -106,9 +115,9 @@ function toConfig(props) {
106
115
  * iframe 事件 → React 回调。名字从 wire 上的小写拼写换成 `onCamelCase`。
107
116
  * 和 vue-frame 的 forward 一一对应(cross-mode-parity):同样的事件、同样的 payload 裁剪。
108
117
  *
109
- * **契约声明的 19 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
118
+ * **契约声明的 27 个事件必须一个不少地出现在这里**,由 `protocol/tests/consumer-surface.contract.test.ts`
110
119
  * 静态断言。曾经漏过 seeking / seeked / waiting / playing 四个,理由是"团队封装层没用到" ——
111
- * 但静态 iframe 模式是裸广播、19 个全发,于是同一份契约在四种接入方式下能收到的事件
120
+ * 但静态 iframe 模式是裸广播、27 个全发,于是同一份契约在四种接入方式下能收到的事件
112
121
  * 不一样,parity 红线实际已破。别再以"业务暂时用不到"为由少接一个。
113
122
  */
114
123
  function forward(event, props) {
@@ -158,18 +167,6 @@ function forward(event, props) {
158
167
  case "autoplayblocked":
159
168
  props.onAutoplayBlocked?.();
160
169
  break;
161
- case "reconnectstart":
162
- props.onReconnectStart?.({
163
- attempt: event.payload.attempt,
164
- maxAttempts: event.payload.maxAttempts
165
- });
166
- break;
167
- case "reconnectsuccess":
168
- props.onReconnectSuccess?.();
169
- break;
170
- case "reconnectfailed":
171
- props.onReconnectFailed?.();
172
- break;
173
170
  case "compatwarning":
174
171
  props.onCompatWarning?.(event.payload);
175
172
  break;
@@ -194,6 +191,9 @@ function forward(event, props) {
194
191
  case "framefreeze":
195
192
  props.onFrameFreeze?.(event.payload);
196
193
  break;
194
+ case "pagefullscreenchange":
195
+ props.onPageFullscreenChange?.(event.payload);
196
+ break;
197
197
  case "useraction":
198
198
  props.onUserAction?.(event.payload);
199
199
  break;
@@ -262,6 +262,7 @@ const VideoPlayerFrame = forwardRef(function VideoPlayerFrame(props, ref) {
262
262
  setMuted: (muted) => handleRef.current?.setMuted(muted) ?? Promise.resolve(),
263
263
  setVolume: (volume) => handleRef.current?.setVolume(volume) ?? Promise.resolve(),
264
264
  setPlaybackRate: (rate) => handleRef.current?.setPlaybackRate(rate) ?? Promise.resolve(),
265
+ setPageFullscreen: (active) => handleRef.current?.setPageFullscreen(active) ?? Promise.reject(/* @__PURE__ */ new Error("播放器未就绪")),
265
266
  enterFullscreen: () => handleRef.current?.enterFullscreen() ?? Promise.resolve(),
266
267
  exitFullscreen: () => handleRef.current?.exitFullscreen() ?? Promise.resolve(),
267
268
  setQuality: (level) => handleRef.current?.setQuality(level) ?? Promise.resolve(),
@@ -270,7 +271,7 @@ const VideoPlayerFrame = forwardRef(function VideoPlayerFrame(props, ref) {
270
271
  pushDanmaku: (item) => handleRef.current?.pushDanmaku(item) ?? Promise.resolve(),
271
272
  setDanmakuEnabled: (enabled) => handleRef.current?.setDanmakuEnabled(enabled) ?? Promise.resolve(),
272
273
  clearDanmaku: () => handleRef.current?.clearDanmaku() ?? Promise.resolve(),
273
- reconnect: (options) => handleRef.current?.reconnect(options) ?? Promise.resolve(),
274
+ retry: () => handleRef.current?.retry() ?? Promise.resolve(),
274
275
  destroy: () => handleRef.current?.destroy() ?? Promise.resolve(),
275
276
  getCurrentTime: () => handleRef.current?.getCurrentTime() ?? 0,
276
277
  getPlaybackContext: () => handleRef.current?.getPlaybackContext() ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@video-lab/react-frame",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -25,7 +25,8 @@
25
25
  "dist/**/*.d.ts",
26
26
  "dist/**/*.d.cts",
27
27
  "dist/**/*.d.mts",
28
- "dist/**/*.css"
28
+ "dist/**/*.css",
29
+ "skills/**"
29
30
  ],
30
31
  "sideEffects": false,
31
32
  "peerDependencies": {
@@ -33,8 +34,8 @@
33
34
  "react-dom": ">=19.0.0"
34
35
  },
35
36
  "dependencies": {
36
- "@video-lab/frame-core": "2.0.0",
37
- "@video-lab/protocol": "2.0.0"
37
+ "@video-lab/protocol": "3.0.1",
38
+ "@video-lab/frame-core": "3.0.1"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@testing-library/jest-dom": "^6.0.0",
@@ -53,7 +54,7 @@
53
54
  "access": "public"
54
55
  },
55
56
  "scripts": {
56
- "build": "tsdown src/index.ts --format esm,cjs --dts --clean",
57
+ "build": "node ../../scripts/sync-public-host-skill.mjs react-frame && tsdown src/index.ts --format esm,cjs --dts --clean",
57
58
  "dev": "tsdown src/index.ts --format esm,cjs --dts --watch --no-clean",
58
59
  "test": "vitest run",
59
60
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit"
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: video-lab-host-integration
3
+ skillVersion: 1
4
+ sdkMajor: 2
5
+ supportedPackages:
6
+ - '@video-lab/react-frame@2.0.0'
7
+ - '@video-lab/vue-frame@2.0.0'
8
+ description: >-
9
+ 在业务宿主项目中接入 Video Lab Player 时使用。选择五种接入面之一,完成可验证 demo,
10
+ 并交接 SDK、宿主与服务端责任;不用于修改 SDK 契约或设计品牌 UI。
11
+ ---
12
+
13
+ # Video Lab Player · 宿主接入
14
+
15
+ ## 取得与版本绑定
16
+
17
+ 在已安装的 `@video-lab/react-frame` 或 `@video-lab/vue-frame` 中读取
18
+ `skills/host-integration/SKILL.md`,再将该目录复制到宿主项目的 AI Skill 目录。先核对
19
+ front matter 的 `supportedPackages` 与安装版本;它是**宿主 AI 的公开接入资料**,不是 npm
20
+ 运行时文件,也不是 iframe 部署目录的一部分。
21
+
22
+ API 以已安装包的类型定义和对应 README 为准;本 Skill 只负责选择接入面与交接流程,不能替代类型。
23
+
24
+ ## 先选接入面
25
+
26
+ | 宿主条件 | 选择 | 不能承诺 |
27
+ | --- | --- | --- |
28
+ | React/Vue 页面,追求性能与深度组合 | `@video-lab/react` / `@video-lab/vue` | 直接操作 xgplayer 私有实例。 |
29
+ | 需要样式、脚本或故障隔离,仍要命令和事件 | `@video-lab/react-frame` / `@video-lab/vue-frame` | 同步读取 iframe DOM 或媒体元素。 |
30
+ | CMS、Markdown、无构建工具,只需嵌入与事件订阅 | 静态 iframe URL,可选 `@video-lab/embed-helper` | 播放命令、换源、字幕、弹幕和实例级握手。 |
31
+
32
+ 若静态 iframe 的能力不够,升级到 React/Vue inline 或 iframe。不要私发 `postMessage`、把 JSON 塞进 query,或猜测未在类型中出现的 SDK API。
33
+
34
+ ## 实施顺序
35
+
36
+ 1. 在宿主项目建立独立 demo 页面,使用服务端提供的完整、已授权 `MediaSource`。不要传 headers、token 刷新回调或业务 ID 给 SDK。
37
+ 2. 以公开 `ready`、`PlayerEvent`、错误码和 `playablechange` 驱动业务状态。runtime Loading 只从 `!playable && recoverable` 派生。跨重连 telemetry、排序、去重和迟到事件识别使用 delivered outlet:React iframe 是 `onDeliveredPlayerEvent`,Vue iframe 是 `@delivered-player-event`;旧 iframe 只能提供 raw `PlayerEvent`。
38
+ 3. 业务服务负责签名 URL、CORS、权益、字幕资源、弹幕审核与限流;品牌 UI、CTA、菜单和无障碍交互留在宿主。
39
+ 4. 业务 telemetry 只记录允许的播放器事实:不要发送完整媒体 URL、query、签名、token、cookie、身份或弹幕正文。
40
+ 5. 用真实或受控媒体覆盖首次加载、自动播放拒绝、可恢复/不可恢复错误、签名换源、字幕失败与卸载清理;恢复必须有时间或画面推进证据。
41
+ 6. 所有可失败命令都 `await` 并 `catch`;命令 rejection 不保证触发 `error`,播放期 `error` 也不保证对应某个命令 rejection。
42
+
43
+ ## 交接模板
44
+
45
+ ```md
46
+ ### Video Lab 宿主接入结论
47
+
48
+ - 接入面:<React inline / Vue inline / React iframe / Vue iframe / 静态 iframe>
49
+ - 选择原因:<隔离、性能或无构建工具约束>
50
+ - 已验证:<源、事件、Loading、恢复、销毁>
51
+ - SDK 已完成:<实际使用的公开能力>
52
+ - 宿主待完成:<UI、状态、telemetry、无障碍、验收>
53
+ - 服务端/CDN 待完成:<签名 URL、CORS、内容、互动治理>
54
+ - 不支持/风险:<模式限制与明确的产品决定>
55
+ ```
56
+
57
+ ## 使用资料的优先级
58
+
59
+ 1. 已安装 `@video-lab/*` 包的 README 和类型定义是 API 的最终事实。
60
+ 2. 接入交接单提供当前的 iframe origin、版本路径、媒体源和部署边界;不得猜测或硬编码内部地址。
61
+ 3. 宿主项目的安全、品牌 UI、业务 telemetry 与服务端约束优先于本 Skill 的通用示例。
62
+
63
+ ## 宿主要求隐藏单个内置控件
64
+
65
+ 使用初始化 `controlVisibility`,例如 `{ cssFullscreen: false }`;静态 iframe 使用 `controlCssFullscreen=0`。
66
+ 保留 `controls` 布尔总开关,不覆盖内部 CSS、不访问私有插件。八字段与重建语义见
67
+ [控件显示指南](../../guides/PLAYER-CONTROL-VISIBILITY.md)。先确认安装版本实际包含该字段,
68
+ iframe 同时核实支持此功能的精确应用版本已部署;旧 iframe 可能忽略新字段。
69
+
70
+ ## 网页全屏交接
71
+
72
+ iframe CSS 全屏必须由宿主布局。四个框架组件传稳定的 `pageFullscreen` 适配器(`setActive`、`dispose`),静态 URL 用 `createPageFullscreenController` 绑定精确 origin 与 iframe。确认、Esc 和销毁清理由 SDK 编排;宿主负责层级、滚动、焦点恢复,不移动 iframe。未接适配器的新 iframe 隐藏入口。旧 iframe 需配套升级,浏览器原生全屏仍独立。以对应包 README 和 `docs/guides/PAGE-FULLSCREEN.md` 为准。