xxf_react 0.5.9 → 0.6.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.
@@ -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,qBAiWpC"}
@@ -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,22 @@ 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
+ * 当视频重新播放时(ended 从 true 变为 false),封面会通过
165
+ * 正常的 checkAndSetReady 流程再次淡出
166
+ */
167
+ useEffect(() => {
168
+ if ((playState.ended || playState.error) && poster) {
169
+ // 播放结束或出错,显示封面
170
+ setShowPoster(true);
171
+ setIsReady(false);
172
+ }
173
+ }, [playState.ended, playState.error, poster]);
166
174
  /**
167
175
  * Effect: 通过 currentTime 检测第一帧(回退方案)
168
176
  *
@@ -265,5 +273,10 @@ export function XVideo({ src, poster, posterFadeOutDuration = 300, bufferingIndi
265
273
  return (React.createElement("div", { className: cn('relative overflow-hidden', className) },
266
274
  React.createElement("video", { ref: videoRef, src: src, preload: preload, crossOrigin: crossOrigin, playsInline: playsInline, className: "w-full h-full object-cover", ...videoProps }),
267
275
  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))));
276
+ isReady && playState.buffering && !playState.error && (bufferingIndicator !== null && bufferingIndicator !== void 0 ? bufferingIndicator : React.createElement(XVideoBufferingIndicator, null)),
277
+ playState.error && (errorIndicator === null
278
+ ? null
279
+ : errorIndicator
280
+ ? errorIndicator(playState.error)
281
+ : React.createElement(XVideoErrorIndicator, { error: playState.error }))));
269
282
  }
@@ -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.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",