@cinecrew/cinecrew-player 0.1.6 → 0.1.8

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
@@ -112,6 +112,7 @@ flowchart LR
112
112
  | MP4 / browser-native media | ✅ Browser media element | ✅ Native engine | A directly playable URL or local URI |
113
113
  | HLS (`.m3u8`) | ✅ hls.js / native HLS where available | ✅ Native engine | Origin access, valid playlist/segments, compatible codecs |
114
114
  | MPEG-TS (`.ts`) | ✅ Bundled patched MPEG-TS client, when browser conditions permit | ✅ VLC path | Browser codecs and CORS; native module availability |
115
+ | Ogg/Theora (`.ogv`) | ✅ `ogv.js` WebAssembly decoder | ✅ Native VLC path | Web uses the versioned ogv.js worker/WASM assets; provide `ogvResourceBase` to self-host them |
115
116
  | Hosted-video or share pages | ⚙️ Resolve to a direct media URL with `resolveSource` | ⚙️ Resolve to a direct media URL with `resolveSource` | A watch/share page is not a media stream |
116
117
  | Local files | ✅ Platform-supported local/blob URI | ✅ Platform-supported file URI | App obtains and passes the platform-readable URI |
117
118
  | Share pages / cloud-drive pages | ⚙️ Optional `resolveSource` | ⚙️ Optional `resolveSource` | Your app resolves authentication and obtains a playable media URL |
@@ -352,6 +353,7 @@ In short: CineCrew’s intended distinction is **one app-facing player package f
352
353
  | `videoOnly` | `boolean` | `false` | Start muted in video-only mode. |
353
354
  | `audioOnly` | `boolean` | `false` | Start in audio-only presentation. Playback continues while the visual card is shown. |
354
355
  | `resolveSource` | callback | — | Optional synchronous or asynchronous resolver for share pages and provider-specific links. Receives `{ uri, ...source }` and `{ platform }`; return a playable URL or `PlayerSource`. Without it, the original source is passed through unchanged. |
356
+ | `ogvResourceBase` | `string` | versioned jsDelivr ogv.js assets | Optional base URL for the OGV decoder’s worker and WebAssembly files. Use this to self-host the matching `ogv@1.9.0/dist/` resources. Web OGV playback is selected by `.ogv` URLs or the `video/ogg` type. |
355
357
  | `audioTracks` | `AudioTrack[]` | detected | Optional supplied track list (web). Native tracks are read from the native player. |
356
358
  | `selectedAudioTrack` | `string \| number` | first/default track | Initial or preferred audio track. |
357
359
  | `resumePosition` | `number` | `0` | Resume position in seconds for on-demand playback. |
@@ -362,7 +364,7 @@ In short: CineCrew’s intended distinction is **one app-facing player package f
362
364
  | `onClose`, `onBack` | callbacks | — | App-owned navigation callbacks; Back does not close the player unless your callback does so. |
363
365
  | `onAspectRatioChange` | `PlayerAction` | — | Top-level callback invoked after the player applies the selected aspect ratio; `actions.onAspectRatioChange` takes precedence if both are supplied. |
364
366
  | `onProgressBarChange` | `(time: string) => void` | — | When the progress bar is shown, reports the played position as zero-padded `HH:MM:SS` once per elapsed playback second, and immediately after a completed seek or restart. Scrubbing reports the committed position, not every intermediate drag update. It is not called when `showProgressBar` is false, `controls.seek` is false, or the live-player UI hides seeking. |
365
- | `onReady`, `onProgress`, `onPlaying`, `onBuffering`, `onError`, `onEnded`, `onPlaybackRoute` | callbacks | — | Playback lifecycle callbacks. Progress payloads are platform-specific native/browser events. |
367
+ | `onReady`, `onProgress`, `onPlaying`, `onBuffering`, `onError`, `onEnded`, `onPlaybackRoute` | callbacks | — | Playback lifecycle callbacks. `onError` receives a player-facing `message` plus `actualMessage` and the original engine error under `cause`/`err` when available; progress payloads are platform-specific native/browser events. |
366
368
  | `onNextEpisode`, `onCwRefresh` | callbacks | — | Episode advancement and post-close refresh hooks. |
367
369
  | `renderLiveChat`, `renderEpg` | render functions | — | Web custom-panel render slots. On native, use the chat/EPG integration adapters. |
368
370
  | `initialShowLiveChat`, `liveChatNonce` | `boolean`, `number` | `false`, `0` | Open or re-open the live-chat panel (when available). |
@@ -370,6 +372,21 @@ In short: CineCrew’s intended distinction is **one app-facing player package f
370
372
 
371
373
  `features={{ diagnostics: true }}` enables the diagnostics button and built-in stream status panel on web; `controls.diagnostics` can hide it. `onFullscreen` receives `{ isFullscreen }` after the player requests the fullscreen change. All lifecycle callbacks in the table are optional; native event objects differ from browser events.
372
374
 
375
+ Use `onError` to receive the same playback failure shown in the player and inspect the original platform/decoder diagnostic. `message` is the player-facing summary; `actualMessage` prefers the underlying error text when the engine provides one, while `cause` and `err` retain the original engine error/event for logging or app-specific reporting:
376
+
377
+ ```tsx
378
+ <CineCrewPlayer
379
+ source={source}
380
+ onError={(error) => {
381
+ const diagnostic = error instanceof Error
382
+ ? error.message
383
+ : error.actualMessage || error.message;
384
+ showSnackbar(diagnostic);
385
+ console.error('Playback error details:', error.cause || error.err || error);
386
+ }}
387
+ />
388
+ ```
389
+
373
390
  ### Player source
374
391
 
375
392
  ```ts
@@ -406,7 +423,7 @@ type PlayerSource = string | {
406
423
  | `actions` | matching `PlayerActions` subset | built-in | Observe play/pause, mute, or fullscreen actions after their built-in behavior runs. |
407
424
  | `initialMuted` | `boolean` | `true` | Initial preview mute state. |
408
425
  | `theme`, `icons`, `style` | `PlayerTheme`, `PlayerIcons`, platform style | defaults | Customize preview colors, controls, and layout. |
409
- | `onError`, `onPlaying` | callbacks | — | Playback lifecycle callbacks. |
426
+ | `onError`, `onPlaying` | callbacks | — | Playback lifecycle callbacks. `onError` includes the underlying `actualMessage` and raw `cause`/`err` when available. |
410
427
 
411
428
  ## Control visibility
412
429
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cinecrew/cinecrew-player",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Customizable cross-platform live and on-demand video player for React and React Native.",
6
6
  "license": "MIT",
@@ -108,12 +108,14 @@
108
108
  "@expo/vector-icons": "^15.1.1",
109
109
  "@mediabunny/ac3": "^1.59.0",
110
110
  "@react-native-community/slider": "^5.2.0",
111
+ "dashjs": "^5.2.1",
111
112
  "expo-video": "^57.0.2",
113
+ "flv.js": "^1.6.2",
112
114
  "hls.js": "^1.7.3",
113
115
  "mediabunny": "^1.59.0",
116
+ "prop-types": "^15.8.1",
114
117
  "react-native-safe-area-context": "^5.7.0",
115
- "react-native-svg": "^15.15.4",
116
- "prop-types": "^15.8.1"
118
+ "react-native-svg": "^15.15.4"
117
119
  },
118
120
  "peerDependencies": {
119
121
  "react": ">=18",
@@ -15,6 +15,7 @@ import { ElectronVideoPlayer } from './media/ElectronVideoPlayer';
15
15
  import { isAndroid, isElectron, isIOS, isWeb } from '../utils/runtimePlatform';
16
16
  import { USER_AGENT } from './media/player/playerConstants';
17
17
  import { invokePlayerAction } from '../utils/invokePlayerAction.js';
18
+ import { getPlayerErrorMessage } from '../utils/playerError.js';
18
19
 
19
20
  function getArtwork(channel) {
20
21
  return channel?.logoUrl || channel?.logo || channel?.stream_icon || channel?.posterUrl || channel?.image || '';
@@ -256,7 +257,16 @@ function InlineLivePlayerView({
256
257
  const message = typeof detail === 'string' ? detail : detail?.message || 'Could not play this channel.';
257
258
  setError(message);
258
259
  setLoading(false);
259
- onError?.(detail instanceof Error ? detail : { ...detail, message });
260
+ if (detail instanceof Error) {
261
+ onError?.(detail);
262
+ return;
263
+ }
264
+ const actualMessage = getPlayerErrorMessage(detail);
265
+ onError?.({
266
+ ...detail,
267
+ message,
268
+ ...(actualMessage ? { actualMessage } : {}),
269
+ });
260
270
  }, [onError]);
261
271
 
262
272
  const handlePlaying = useCallback((event) => {
@@ -3,7 +3,10 @@ import { View, StyleSheet } from 'react-native';
3
3
  import { useWebMediaSession } from './web/useWebMediaSession';
4
4
  import { useWebVideoAspectRatio } from './web/useWebVideoAspectRatio';
5
5
  import { useWebMpegTsPlayback } from './web/useWebMpegTsPlayback';
6
+ import { useWebFlvPlayback } from './web/useWebFlvPlayback';
6
7
  import { useWebHlsPlayback } from './web/useWebHlsPlayback';
8
+ import { useWebDashPlayback } from './web/useWebDashPlayback';
9
+ import { useWebOgvPlayback } from './web/useWebOgvPlayback';
7
10
  import { useWebAc3AudioPlayback } from './web/useWebAc3AudioPlayback';
8
11
 
9
12
  export const WebVideoPlayer = forwardRef(({
@@ -27,8 +30,10 @@ export const WebVideoPlayer = forwardRef(({
27
30
  onTogglePlayPause,
28
31
  onSeekBy,
29
32
  onPlaybackRoute,
33
+ ogvResourceBase,
30
34
  }, ref) => {
31
35
  const videoRef = useRef(null);
36
+ const ogvContainerRef = useRef(null);
32
37
  const pausedRef = useRef(paused);
33
38
  const playbackRateRef = useRef(playbackRate);
34
39
  const lastKnownPosRef = useRef(0);
@@ -38,6 +43,8 @@ export const WebVideoPlayer = forwardRef(({
38
43
  const onErrorRef = useRef(onError);
39
44
  const playbackErrorRef = useRef(onError);
40
45
  const onEndedRef = useRef(onEnded);
46
+ const onProgressRef = useRef(null);
47
+ const onPlaybackRouteEventRef = useRef(null);
41
48
  const [activeUrl, setActiveUrl] = useState('');
42
49
  const [corsMode, setCorsMode] = useState('anonymous');
43
50
  const corsModeRef = useRef('anonymous');
@@ -65,6 +72,15 @@ export const WebVideoPlayer = forwardRef(({
65
72
  onErrorRef: playbackErrorRef,
66
73
  onBufferingRef,
67
74
  });
75
+ const useFlvSource = useWebFlvPlayback({
76
+ activeUrl,
77
+ type: streamUrl,
78
+ isLive,
79
+ videoRef,
80
+ pausedRef,
81
+ onErrorRef: playbackErrorRef,
82
+ onBufferingRef,
83
+ });
68
84
  const useMpegTsSource = mpegTsPlayback.useMpegTs;
69
85
  const useAc3Fallback = mpegTsPlayback.useAc3Fallback;
70
86
  const useHlsSource = useWebHlsPlayback({
@@ -74,6 +90,33 @@ export const WebVideoPlayer = forwardRef(({
74
90
  pausedRef,
75
91
  onErrorRef: playbackErrorRef,
76
92
  });
93
+ const useDashSource = useWebDashPlayback({
94
+ activeUrl,
95
+ videoRef,
96
+ pausedRef,
97
+ onErrorRef: playbackErrorRef,
98
+ });
99
+ const { videoStyle, applyAspectRatio } = useWebVideoAspectRatio(videoAspectRatio, audioOnly);
100
+ const useOgvSource = useWebOgvPlayback({
101
+ activeUrl,
102
+ type: streamUrl,
103
+ videoRef,
104
+ playerContainerRef: ogvContainerRef,
105
+ playerStyle: videoStyle,
106
+ pausedRef,
107
+ onErrorRef: playbackErrorRef,
108
+ onBufferingRef,
109
+ resourceBase: ogvResourceBase,
110
+ paused,
111
+ muted,
112
+ volume: (volume ?? 100) / 100,
113
+ playbackRate,
114
+ videoOnly,
115
+ onProgressRef,
116
+ onPlayingRef,
117
+ onEndedRef,
118
+ onPlaybackRouteRef: onPlaybackRouteEventRef,
119
+ });
77
120
  const ac3Audio = useWebAc3AudioPlayback({
78
121
  active: useAc3Fallback,
79
122
  streamUrl: activeUrl,
@@ -84,10 +127,13 @@ export const WebVideoPlayer = forwardRef(({
84
127
  });
85
128
 
86
129
  // 1. Aspect Ratio Styling
87
- const { videoStyle, applyAspectRatio } = useWebVideoAspectRatio(videoAspectRatio, audioOnly);
88
- const videoSource = useMpegTsSource || useHlsSource ? undefined : activeUrl;
89
- let streamMode = useHlsSource ? 'hls' : 'native';
90
- if (useMpegTsSource) streamMode = 'mpegts';
130
+ const videoSource = useMpegTsSource || useFlvSource || useHlsSource || useDashSource || useOgvSource ? undefined : activeUrl;
131
+ let streamMode = 'native';
132
+ if (mpegTsPlayback.isFlv) streamMode = 'flv';
133
+ else if (useMpegTsSource) streamMode = 'mpegts';
134
+ else if (useHlsSource) streamMode = 'hls';
135
+ else if (useDashSource) streamMode = 'dash';
136
+ else if (useOgvSource) streamMode = 'ogv';
91
137
  const resolvedScheme = new URL(activeUrl || 'http://localhost', 'http://localhost').protocol.replace(':', '');
92
138
 
93
139
  // Report active streaming route
@@ -120,6 +166,11 @@ export const WebVideoPlayer = forwardRef(({
120
166
  });
121
167
  }
122
168
  }, [onProgress]);
169
+ onProgressRef.current = emitProgress;
170
+ onPlaybackRouteEventRef.current = (attachedUrl) => {
171
+ const emit = onPlaybackRouteRef.current;
172
+ if (typeof emit === 'function' && attachedUrl) emit(attachedUrl);
173
+ };
123
174
 
124
175
  // The host supplies the media URL. Scheme and source resolution are left to
125
176
  // the consumer and the playback engine on each platform.
@@ -161,7 +212,7 @@ export const WebVideoPlayer = forwardRef(({
161
212
  ) {
162
213
  video.play().catch(() => {});
163
214
  }
164
- }, [paused, activeUrl]);
215
+ }, [paused, activeUrl, corsMode]);
165
216
 
166
217
  // Gain and volume
167
218
  useEffect(() => {
@@ -237,7 +288,22 @@ export const WebVideoPlayer = forwardRef(({
237
288
 
238
289
  return (
239
290
  <View style={styles.container} collapsable={false}>
240
- {activeUrl ? (
291
+ {activeUrl && (
292
+ useOgvSource ? (
293
+ <div
294
+ key={activeUrl}
295
+ ref={ogvContainerRef}
296
+ data-resolved-scheme={resolvedScheme}
297
+ data-stream-mode="ogv"
298
+ className="cinecrew-player__video"
299
+ style={{
300
+ ...StyleSheet.absoluteFillObject,
301
+ width: '100%',
302
+ height: '100%',
303
+ backgroundColor: '#000',
304
+ }}
305
+ />
306
+ ) : (
241
307
  <video
242
308
  key={`${activeUrl}:${corsMode || 'nocors'}`}
243
309
  ref={videoRef}
@@ -317,7 +383,8 @@ export const WebVideoPlayer = forwardRef(({
317
383
  />
318
384
  ))}
319
385
  </video>
320
- ) : null}
386
+ )
387
+ )}
321
388
  </View>
322
389
  );
323
390
  });
@@ -0,0 +1,3 @@
1
+ export function useWebDashPlayback() {
2
+ return false;
3
+ }
@@ -0,0 +1,84 @@
1
+ import { useEffect } from 'react';
2
+
3
+ const isDashUrl = (url, type) => {
4
+ if (type && /dash/i.test(type)) return true;
5
+ return /\.mpd(?:$|[?#])/i.test(String(url || ''));
6
+ };
7
+
8
+ function handleAutoplayFailure(video, error) {
9
+ if (error?.name !== 'NotAllowedError') return;
10
+ video.muted = true;
11
+ video.play().catch(() => {});
12
+ }
13
+
14
+ export function useWebDashPlayback({
15
+ activeUrl,
16
+ type,
17
+ videoRef,
18
+ pausedRef,
19
+ onErrorRef,
20
+ }) {
21
+ const useDash = Boolean(isDashUrl(activeUrl, type));
22
+
23
+ useEffect(() => {
24
+ if (!useDash || !activeUrl || typeof window === 'undefined') return undefined;
25
+ const video = videoRef.current;
26
+ if (!video) return undefined;
27
+
28
+ let player = null;
29
+ let disposed = false;
30
+
31
+ import('dashjs')
32
+ .then((mod) => {
33
+ if (disposed) return;
34
+ const dashjs = mod.default || mod;
35
+ if (!dashjs?.MediaPlayer) {
36
+ onErrorRef.current?.({ message: 'Dash.js is not supported in this browser.' });
37
+ return;
38
+ }
39
+
40
+ player = dashjs.MediaPlayer().create();
41
+ video.crossOrigin = 'anonymous';
42
+ player.initialize(video, activeUrl, !pausedRef.current);
43
+
44
+ player.on(dashjs.MediaPlayer.events.ERROR, (event) => {
45
+ if (disposed) return;
46
+ const status = Number(event?.error?.data?.response?.status || event?.data?.status);
47
+ const httpStatus = Number.isFinite(status) && status >= 100 ? status : null;
48
+ const errorMsg = httpStatus
49
+ ? `Stream request failed (HTTP ${httpStatus}).`
50
+ : `MPEG-DASH playback failed (${String(event?.error?.message || event?.message || 'playback error')}).`;
51
+ onErrorRef.current?.({ message: errorMsg, httpStatus, err: event });
52
+ });
53
+
54
+ const attemptAutoplay = () => {
55
+ if (disposed || pausedRef.current) return;
56
+ const playPromise = video.play();
57
+ playPromise?.catch((err) => handleAutoplayFailure(video, err));
58
+ };
59
+
60
+ player.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, attemptAutoplay);
61
+ player.on(dashjs.MediaPlayer.events.CAN_PLAY, attemptAutoplay);
62
+ })
63
+ .catch((err) => {
64
+ if (!disposed) {
65
+ onErrorRef.current?.({ message: 'Failed to initialize MPEG-DASH engine.', err });
66
+ }
67
+ });
68
+
69
+ return () => {
70
+ disposed = true;
71
+ if (player) {
72
+ try {
73
+ player.destroy();
74
+ } catch {}
75
+ } else if (video.src === activeUrl) {
76
+ video.pause();
77
+ video.removeAttribute('src');
78
+ video.load();
79
+ }
80
+ };
81
+ }, [activeUrl, useDash, videoRef, pausedRef, onErrorRef]);
82
+
83
+ return useDash;
84
+ }
@@ -0,0 +1,156 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import flvjs from 'flv.js';
3
+ import { getPlayerErrorMessage } from '../../../utils/playerError.js';
4
+
5
+ const isFlvSource = (url, type) => /flv/i.test(String(type || ''))
6
+ || /\.flv(?:$|[?#])/i.test(String(url || ''))
7
+ || /^(?:ws|wss):/i.test(String(url || ''));
8
+
9
+ function attemptPlayback(video, pausedRef, onAutoplayBlockedRef) {
10
+ if (pausedRef.current) return;
11
+ const playResult = video.play();
12
+ playResult?.catch?.((error) => {
13
+ if (error?.name !== 'NotAllowedError') return;
14
+ // Autoplay may be blocked until a user gesture. Keep the requested audio
15
+ // state intact and expose the player's normal Play control for that gesture.
16
+ video.pause();
17
+ pausedRef.current = true;
18
+ onAutoplayBlockedRef.current?.();
19
+ });
20
+ }
21
+
22
+ export function useWebFlvPlayback({
23
+ activeUrl,
24
+ type,
25
+ isLive,
26
+ videoRef,
27
+ pausedRef,
28
+ onErrorRef,
29
+ onBufferingRef,
30
+ onAutoplayBlocked,
31
+ }) {
32
+ const useFlv = Boolean(isFlvSource(activeUrl, type));
33
+ const onAutoplayBlockedRef = useRef(onAutoplayBlocked);
34
+ onAutoplayBlockedRef.current = onAutoplayBlocked;
35
+
36
+ useEffect(() => {
37
+ if (!useFlv || !activeUrl || typeof window === 'undefined') return undefined;
38
+ const video = videoRef.current;
39
+ if (!video) return undefined;
40
+
41
+ const flv = flvjs?.default || flvjs;
42
+ if (!flv?.isSupported?.()) {
43
+ onErrorRef.current?.({ message: 'FLV playback is not supported in this browser.' });
44
+ return undefined;
45
+ }
46
+
47
+ let disposed = false;
48
+ let playbackErrorReported = false;
49
+ let specificCodecErrorReported = false;
50
+ let player;
51
+ const handleCanPlay = () => {
52
+ onBufferingRef?.current?.(false);
53
+ attemptPlayback(video, pausedRef, onAutoplayBlockedRef);
54
+ };
55
+ const handleWaiting = () => onBufferingRef?.current?.(true);
56
+ const handleVideoError = () => {
57
+ if (disposed || specificCodecErrorReported || video.readyState >= 2) return;
58
+ const mediaError = video.error;
59
+ onBufferingRef?.current?.(false);
60
+ onErrorRef.current?.({
61
+ message: mediaError?.message || 'The browser could not decode the FLV stream.',
62
+ err: mediaError,
63
+ });
64
+ };
65
+
66
+ try {
67
+ player = flv.createPlayer({
68
+ type: 'flv',
69
+ url: activeUrl,
70
+ isLive: Boolean(isLive),
71
+ hasVideo: true,
72
+ }, {
73
+ // flv.js's worker bundle is webpack-specific and can fail to initialize
74
+ // under consumer bundlers such as Vite/Metro. Inline transmuxing is
75
+ // reliable for this bounded remux operation and matches the package's
76
+ // default behavior.
77
+ enableWorker: false,
78
+ enableStashBuffer: true,
79
+ stashInitialSize: isLive ? 384 * 1024 : 1024 * 1024,
80
+ lazyLoad: false,
81
+ autoCleanupSourceBuffer: Boolean(isLive),
82
+ });
83
+
84
+ player.on(flv.Events.ERROR, (errorType, errorDetail, errorInfo) => {
85
+ if (disposed || specificCodecErrorReported) return;
86
+ const details = [errorType, errorDetail, errorInfo?.msg, errorInfo?.message]
87
+ .filter(Boolean)
88
+ .join(' ');
89
+ const unsupportedAudio = /unsupported codec in audio frame|unsupported audio codec|audio.*(?:unsupported|not supported)|(?:unsupported|not supported).*audio/i.test(details);
90
+ const unsupportedVideo = /unsupported codec in video frame|unsupported.*video|video.*(?:unsupported|not supported).*codec/i.test(details);
91
+ // flv.js rejects individual audio packets with codecs other than AAC
92
+ // and MP3, but can continue transmuxing the remaining supported audio
93
+ // and video packets. Don't turn a skippable packet into a fatal player
94
+ // error or disable the stream's supported audio track.
95
+ if (unsupportedAudio && !unsupportedVideo) return;
96
+ if (playbackErrorReported && !unsupportedAudio && !unsupportedVideo) return;
97
+ playbackErrorReported = true;
98
+ specificCodecErrorReported = unsupportedAudio || unsupportedVideo;
99
+ const status = Number(errorInfo?.code || errorInfo?.status);
100
+ const httpStatus = Number.isFinite(status) && status >= 100 ? status : null;
101
+ let message;
102
+ if (httpStatus) {
103
+ message = `FLV stream request failed (HTTP ${httpStatus}).`;
104
+ } else if (unsupportedAudio) {
105
+ message = 'This FLV audio codec is not supported by the browser player. Use AAC or MP3 audio in the FLV stream.';
106
+ } else if (unsupportedVideo) {
107
+ message = 'This FLV video codec is not supported by the browser player. Use H.264/AVC video in the FLV stream.';
108
+ } else {
109
+ message = `FLV playback failed (${String(errorDetail || errorType || 'unknown error')}).`;
110
+ }
111
+ const actualMessage = getPlayerErrorMessage(errorInfo)
112
+ || [errorType, errorDetail].filter(Boolean).map(String).join(': ')
113
+ || message;
114
+ onBufferingRef?.current?.(false);
115
+ onErrorRef.current?.({
116
+ message,
117
+ actualMessage,
118
+ errorType: String(errorType || ''),
119
+ errorDetail: String(errorDetail || ''),
120
+ httpStatus,
121
+ cause: errorInfo || { errorType, errorDetail },
122
+ // Keep the existing alias for consumers that already inspect it.
123
+ err: errorInfo || { errorType, errorDetail },
124
+ });
125
+ });
126
+
127
+ video.crossOrigin = 'anonymous';
128
+ video.addEventListener('canplay', handleCanPlay);
129
+ video.addEventListener('waiting', handleWaiting);
130
+ video.addEventListener('error', handleVideoError);
131
+ player.attachMediaElement(video);
132
+ player.load();
133
+ attemptPlayback(video, pausedRef, onAutoplayBlockedRef);
134
+ } catch (error) {
135
+ onBufferingRef?.current?.(false);
136
+ onErrorRef.current?.({
137
+ message: error?.message || 'Failed to initialize FLV playback.',
138
+ err: error,
139
+ });
140
+ }
141
+
142
+ return () => {
143
+ disposed = true;
144
+ video.removeEventListener('canplay', handleCanPlay);
145
+ video.removeEventListener('waiting', handleWaiting);
146
+ video.removeEventListener('error', handleVideoError);
147
+ if (!player) return;
148
+ try { player.pause(); } catch {}
149
+ try { player.unload(); } catch {}
150
+ try { player.detachMediaElement(); } catch {}
151
+ try { player.destroy(); } catch {}
152
+ };
153
+ }, [activeUrl, isLive, useFlv, videoRef, pausedRef, onErrorRef, onBufferingRef]);
154
+
155
+ return useFlv;
156
+ }
@@ -4,7 +4,7 @@ import Hls from 'hls.js';
4
4
  const isHlsUrl = (url) => /\.m3u8(?:$|[?#])/i.test(String(url || ''));
5
5
 
6
6
  export function useWebHlsPlayback({ activeUrl, isLive, videoRef, pausedRef, onErrorRef }) {
7
- const useHls = isLive && isHlsUrl(activeUrl);
7
+ const useHls = Boolean(isHlsUrl(activeUrl));
8
8
 
9
9
  useEffect(() => {
10
10
  if (!useHls || !activeUrl || typeof window === 'undefined') return undefined;
@@ -43,10 +43,33 @@ export function useWebHlsPlayback({ activeUrl, isLive, videoRef, pausedRef, onEr
43
43
  hls.loadSource(activeUrl);
44
44
  video.crossOrigin = 'anonymous';
45
45
  hls.attachMedia(video);
46
+ const attemptPlay = () => {
47
+ if (pausedRef.current) return;
48
+ const playPromise = video.play();
49
+ if (playPromise !== undefined) {
50
+ playPromise.catch((err) => {
51
+ if (err?.name === 'NotAllowedError') {
52
+ video.muted = true;
53
+ video.play().catch(() => {});
54
+ }
55
+ });
56
+ }
57
+ };
58
+ hls.on(Hls.Events.MANIFEST_PARSED, attemptPlay);
46
59
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
47
60
  video.src = activeUrl;
48
61
  video.load();
49
- if (!pausedRef.current) video.play().catch(() => {});
62
+ if (!pausedRef.current) {
63
+ const playPromise = video.play();
64
+ if (playPromise !== undefined) {
65
+ playPromise.catch((err) => {
66
+ if (err?.name === 'NotAllowedError') {
67
+ video.muted = true;
68
+ video.play().catch(() => {});
69
+ }
70
+ });
71
+ }
72
+ }
50
73
  } else {
51
74
  onErrorRef.current?.({ message: 'This browser does not support HLS playback.' });
52
75
  }
@@ -1,3 +1,3 @@
1
1
  export function useWebMpegTsPlayback() {
2
- return { useMpegTs: false, useAc3Fallback: false };
2
+ return { useMpegTs: false, useAc3Fallback: false, isFlv: false };
3
3
  }