xxf_react 0.5.9 → 0.6.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.
@@ -74,6 +74,35 @@ export interface XVideoProps extends Omit<VideoHTMLAttributes<HTMLVideoElement>,
74
74
  * ```
75
75
  */
76
76
  bufferingIndicator?: ReactNode;
77
+ /**
78
+ * 错误指示器回调(可选)
79
+ *
80
+ * - 视频加载或播放失败时调用此回调渲染错误 UI
81
+ * - 接收 MediaError 参数,可根据错误类型显示不同内容
82
+ * - 不传则使用默认的错误提示(显示封面 + 错误图标)
83
+ * - 传 null 可禁用错误指示器
84
+ *
85
+ * MediaError.code 错误码:
86
+ * - 1 (MEDIA_ERR_ABORTED): 用户中止
87
+ * - 2 (MEDIA_ERR_NETWORK): 网络错误
88
+ * - 3 (MEDIA_ERR_DECODE): 解码错误
89
+ * - 4 (MEDIA_ERR_SRC_NOT_SUPPORTED): 格式不支持
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * // 根据错误类型显示不同提示
94
+ * <XVideo
95
+ * src={videoUrl}
96
+ * errorIndicator={(error) => (
97
+ * <div>{error.code === 2 ? '网络错误' : '播放失败'}</div>
98
+ * )}
99
+ * />
100
+ *
101
+ * // 禁用错误指示器
102
+ * <XVideo src={videoUrl} errorIndicator={null} />
103
+ * ```
104
+ */
105
+ errorIndicator?: ((error: MediaError) => ReactNode) | null;
77
106
  /**
78
107
  * 容器 className(可选)
79
108
  *
@@ -172,5 +201,5 @@ export interface XVideoProps extends Omit<VideoHTMLAttributes<HTMLVideoElement>,
172
201
  * <XVideo src={videoUrl} videoRef={videoRef} poster={coverUrl} />
173
202
  * ```
174
203
  */
175
- export declare function XVideo({ src, poster, posterFadeOutDuration, bufferingIndicator, className, videoRef: externalVideoRef, onReady, preload, playsInline, crossOrigin, ...videoProps }: XVideoProps): React.JSX.Element;
204
+ export declare function XVideo({ src, poster, posterFadeOutDuration, bufferingIndicator, errorIndicator, className, videoRef: externalVideoRef, onReady, preload, playsInline, crossOrigin, ...videoProps }: XVideoProps): React.JSX.Element;
176
205
  //# sourceMappingURL=XVideo.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"XVideo.d.ts","sourceRoot":"","sources":["../../../src/media/components/XVideo.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAMV,SAAS,EACT,mBAAmB,EACnB,SAAS,EACZ,MAAM,OAAO,CAAA;AAKd;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC;IACtF;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAE3B;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAE9B;;;;;;;;;;;;;;;;;;OAkBG;IACH,kBAAkB,CAAC,EAAE,SAAS,CAAA;IAE9B;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAE7C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,MAAM,CAAC,EACI,GAAG,EACH,MAAM,EACN,qBAA2B,EAC3B,kBAAkB,EAClB,SAAS,EACT,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EACP,OAAgB,EAChB,WAAkB,EAClB,WAAyB,EACzB,GAAG,UAAU,EAChB,EAAE,WAAW,qBA6TpC"}
1
+ {"version":3,"file":"XVideo.d.ts","sourceRoot":"","sources":["../../../src/media/components/XVideo.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAMV,SAAS,EACT,mBAAmB,EACnB,SAAS,EACZ,MAAM,OAAO,CAAA;AAOd;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC;IACtF;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAE3B;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAE9B;;;;;;;;;;;;;;;;;;OAkBG;IACH,kBAAkB,CAAC,EAAE,SAAS,CAAA;IAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,SAAS,CAAC,GAAG,IAAI,CAAA;IAE1D;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAE7C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,MAAM,CAAC,EACI,GAAG,EACH,MAAM,EACN,qBAA2B,EAC3B,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EACP,OAAgB,EAChB,WAAkB,EAClB,WAAyB,EACzB,GAAG,UAAU,EAChB,EAAE,WAAW,qBAgXpC"}
@@ -3,16 +3,8 @@ import React, { useRef, useState, useEffect, useCallback, useMemo, } from 'react
3
3
  import { cn } from 'tailwind-variants';
4
4
  import { XImage } from 'xxf_react';
5
5
  import { useVideoState } from 'xxf_react/media';
6
- /**
7
- * 默认缓冲指示器组件
8
- *
9
- * 显示一个居中的旋转 loading spinner
10
- * 使用 React.memo 优化,避免不必要的重渲染
11
- */
12
- const DefaultBufferingIndicator = React.memo(function DefaultBufferingIndicator() {
13
- return (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none" },
14
- React.createElement("div", { className: "w-8 h-8 border-4 border-white/30 border-t-white rounded-full animate-spin" })));
15
- });
6
+ import { XVideoBufferingIndicator } from './XVideoBufferingIndicator';
7
+ import { XVideoErrorIndicator } from './XVideoErrorIndicator';
16
8
  /**
17
9
  * XVideo - 通用视频组件
18
10
  *
@@ -50,7 +42,7 @@ const DefaultBufferingIndicator = React.memo(function DefaultBufferingIndicator(
50
42
  * <XVideo src={videoUrl} videoRef={videoRef} poster={coverUrl} />
51
43
  * ```
52
44
  */
53
- export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndicator, className, videoRef: externalVideoRef, onReady, preload = 'auto', playsInline = true, crossOrigin = "anonymous", ...videoProps }) {
45
+ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndicator, errorIndicator, className, videoRef: externalVideoRef, onReady, preload = 'auto', playsInline = true, crossOrigin = "anonymous", ...videoProps }) {
54
46
  // ==================== Refs ====================
55
47
  /**
56
48
  * 内部 video ref
@@ -163,6 +155,30 @@ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndi
163
155
  useEffect(() => {
164
156
  checkAndSetReady();
165
157
  }, [playState.canPlay, checkAndSetReady]);
158
+ /**
159
+ * Effect: 暂停、播放结束或出错时显示封面
160
+ *
161
+ * 视频暂停时,重新显示封面
162
+ * 视频播放结束时,重新显示封面,避免最后一帧可能是黑屏的问题
163
+ * 视频播放出错时,重新显示封面,作为错误指示器的背景
164
+ *
165
+ * 当视频重新播放时,封面会通过正常的 checkAndSetReady 流程再次淡出
166
+ *
167
+ * 注意:必须同时重置 firstFrameRenderedRef,否则 checkAndSetReady 会立即再次隐藏封面
168
+ */
169
+ useEffect(() => {
170
+ if (poster && (playState.ended || playState.error || !playState.playing)) {
171
+ // 清理淡出定时器,防止正在进行的淡出继续执行
172
+ if (fadeOutTimerRef.current) {
173
+ clearTimeout(fadeOutTimerRef.current);
174
+ fadeOutTimerRef.current = null;
175
+ }
176
+ // 重置第一帧标记,防止 checkAndSetReady 立即隐藏封面
177
+ firstFrameRenderedRef.current = false;
178
+ setShowPoster(true);
179
+ setIsReady(false);
180
+ }
181
+ }, [playState.ended, playState.error, playState.playing, poster]);
166
182
  /**
167
183
  * Effect: 通过 currentTime 检测第一帧(回退方案)
168
184
  *
@@ -170,13 +186,15 @@ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndi
170
186
  * 使用 currentTime > 0 作为第一帧渲染的近似检测
171
187
  *
172
188
  * 原理:currentTime > 0 表示视频已开始播放,此时第一帧大概率已渲染
189
+ *
190
+ * 注意:必须检查 playState.playing,否则暂停时 currentTime > 0 会立即触发封面隐藏
173
191
  */
174
192
  useEffect(() => {
175
- if (!firstFrameRenderedRef.current && playState.currentTime > 0) {
193
+ if (!firstFrameRenderedRef.current && playState.currentTime > 0 && playState.playing) {
176
194
  firstFrameRenderedRef.current = true;
177
195
  checkAndSetReady();
178
196
  }
179
- }, [playState.currentTime, checkAndSetReady]);
197
+ }, [playState.currentTime, playState.playing, checkAndSetReady]);
180
198
  /**
181
199
  * Effect: 第一帧检测 - 优先使用 requestVideoFrameCallback
182
200
  *
@@ -189,7 +207,9 @@ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndi
189
207
  * 3. 第一帧渲染时标记 firstFrameRendered 并检查就绪状态
190
208
  * 4. cleanup 时取消回调,防止组件卸载后执行
191
209
  *
192
- * 注意:cleanedUp flag 用于防止组件卸载后的状态更新(React 严格模式下的竞态条件)
210
+ * 注意:
211
+ * - cleanedUp flag 用于防止组件卸载后的状态更新(React 严格模式下的竞态条件)
212
+ * - 必须检查 playState.playing,否则暂停时可能错误触发
193
213
  */
194
214
  useEffect(() => {
195
215
  const video = videoRef.current;
@@ -198,6 +218,9 @@ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndi
198
218
  // 如果已经检测到第一帧,无需重复检测
199
219
  if (firstFrameRenderedRef.current)
200
220
  return;
221
+ // 视频未播放时不注册回调
222
+ if (!playState.playing)
223
+ return;
201
224
  // 检查浏览器是否支持 requestVideoFrameCallback
202
225
  const supportsVideoFrameCallback = 'requestVideoFrameCallback' in HTMLVideoElement.prototype;
203
226
  if (supportsVideoFrameCallback) {
@@ -218,7 +241,7 @@ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndi
218
241
  };
219
242
  }
220
243
  // 不支持 requestVideoFrameCallback 的浏览器由 currentTime effect 处理
221
- }, [src, videoRef, checkAndSetReady]);
244
+ }, [src, videoRef, playState.playing, checkAndSetReady]);
222
245
  // ==================== 渲染逻辑 ====================
223
246
  /**
224
247
  * 封面元素 - 使用 useMemo 优化
@@ -265,5 +288,10 @@ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndi
265
288
  return (React.createElement("div", { className: cn('relative overflow-hidden', className) },
266
289
  React.createElement("video", { ref: videoRef, src: src, preload: preload, crossOrigin: crossOrigin, playsInline: playsInline, className: "w-full h-full object-cover", ...videoProps }),
267
290
  posterElement && showPoster && (React.createElement("div", { className: cn('absolute inset-0 transition-opacity', isReady ? 'opacity-0 pointer-events-none' : 'opacity-100'), style: { transitionDuration: `${posterFadeOutDuration}ms` } }, posterElement)),
268
- isReady && playState.buffering && (bufferingIndicator !== null && bufferingIndicator !== void 0 ? bufferingIndicator : React.createElement(DefaultBufferingIndicator, null))));
291
+ isReady && playState.buffering && !playState.error && (bufferingIndicator !== null && bufferingIndicator !== void 0 ? bufferingIndicator : React.createElement(XVideoBufferingIndicator, null)),
292
+ playState.error && (errorIndicator === null
293
+ ? null
294
+ : errorIndicator
295
+ ? errorIndicator(playState.error)
296
+ : React.createElement(XVideoErrorIndicator, { error: playState.error }))));
269
297
  }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ export interface XVideoBufferingIndicatorProps {
3
+ /** 自定义样式类名 */
4
+ className?: string;
5
+ }
6
+ /**
7
+ * XVideo 默认缓冲指示器组件
8
+ *
9
+ * 显示一个居中的旋转 loading spinner
10
+ * 使用 React.memo 优化,避免不必要的重渲染
11
+ */
12
+ export declare const XVideoBufferingIndicator: React.NamedExoticComponent<XVideoBufferingIndicatorProps>;
13
+ //# sourceMappingURL=XVideoBufferingIndicator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XVideoBufferingIndicator.d.ts","sourceRoot":"","sources":["../../../src/media/components/XVideoBufferingIndicator.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,MAAM,WAAW,6BAA6B;IAC1C,cAAc;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,2DAQnC,CAAA"}
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import React from 'react';
3
+ import { cn } from 'tailwind-variants';
4
+ /**
5
+ * XVideo 默认缓冲指示器组件
6
+ *
7
+ * 显示一个居中的旋转 loading spinner
8
+ * 使用 React.memo 优化,避免不必要的重渲染
9
+ */
10
+ export const XVideoBufferingIndicator = React.memo(function XVideoBufferingIndicator({ className } = {}) {
11
+ return (React.createElement("div", { className: cn('absolute inset-0 flex items-center justify-center pointer-events-none', className) },
12
+ React.createElement("div", { className: "w-8 h-8 border-4 border-white/30 border-t-white rounded-full animate-spin" })));
13
+ });
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ export interface XVideoErrorIndicatorProps {
3
+ /** 视频播放错误对象 */
4
+ error: MediaError;
5
+ /** 自定义样式类名 */
6
+ className?: string;
7
+ }
8
+ /**
9
+ * XVideo 默认错误指示器组件
10
+ *
11
+ * 显示一个居中的错误图标和错误信息(最多50字符)
12
+ * 使用 React.memo 优化,避免不必要的重渲染
13
+ */
14
+ export declare const XVideoErrorIndicator: React.NamedExoticComponent<XVideoErrorIndicatorProps>;
15
+ //# sourceMappingURL=XVideoErrorIndicator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XVideoErrorIndicator.d.ts","sourceRoot":"","sources":["../../../src/media/components/XVideoErrorIndicator.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,MAAM,WAAW,yBAAyB;IACtC,eAAe;IACf,KAAK,EAAE,UAAU,CAAA;IACjB,cAAc;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,uDA6B/B,CAAA"}
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import React from 'react';
3
+ import { cn } from 'tailwind-variants';
4
+ /**
5
+ * XVideo 默认错误指示器组件
6
+ *
7
+ * 显示一个居中的错误图标和错误信息(最多50字符)
8
+ * 使用 React.memo 优化,避免不必要的重渲染
9
+ */
10
+ export const XVideoErrorIndicator = React.memo(function XVideoErrorIndicator({ error, className }) {
11
+ const errorMessage = error.message
12
+ ? error.message.slice(0, 50) + (error.message.length > 50 ? '...' : '')
13
+ : 'Video load failed';
14
+ const message = `code:${error.code} ${errorMessage}`;
15
+ return (React.createElement("div", { className: cn('absolute inset-0 flex flex-col items-center justify-center bg-black/60 pointer-events-none', className) },
16
+ React.createElement("svg", { className: "w-12 h-12 text-white/80", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5 },
17
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" })),
18
+ React.createElement("span", { className: "mt-2 text-sm text-white/80 px-4 text-center" }, message)));
19
+ });
@@ -1,4 +1,6 @@
1
1
  export * from './video-state';
2
2
  export * from './playback-queue-store';
3
3
  export * from './components/XVideo';
4
+ export * from './components/XVideoBufferingIndicator';
5
+ export * from './components/XVideoErrorIndicator';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAEA,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAEA,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC"}
@@ -2,3 +2,5 @@
2
2
  export * from './video-state';
3
3
  export * from './playback-queue-store';
4
4
  export * from './components/XVideo';
5
+ export * from './components/XVideoBufferingIndicator';
6
+ export * from './components/XVideoErrorIndicator';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xxf_react",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",