@ttt-productions/media-viewer 0.10.9 → 0.10.11
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/dist/react/audio-viewer.d.ts.map +1 -1
- package/dist/react/audio-viewer.js +8 -2
- package/dist/react/audio-viewer.js.map +1 -1
- package/dist/react/media-viewer.d.ts.map +1 -1
- package/dist/react/media-viewer.js +2 -2
- package/dist/react/media-viewer.js.map +1 -1
- package/dist/react/use-media-playback.d.ts +33 -0
- package/dist/react/use-media-playback.d.ts.map +1 -1
- package/dist/react/use-media-playback.js +120 -9
- package/dist/react/use-media-playback.js.map +1 -1
- package/dist/react/video-viewer.d.ts.map +1 -1
- package/dist/react/video-viewer.js +12 -3
- package/dist/react/video-viewer.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-viewer.d.ts","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"audio-viewer.d.ts","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBAkLlD"}
|
|
@@ -13,7 +13,13 @@ export function AudioViewer(props) {
|
|
|
13
13
|
const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);
|
|
14
14
|
const audioRef = React.useRef(null);
|
|
15
15
|
// Additive playback API — derives from element events (no observer, MEDIA-102).
|
|
16
|
-
const { hasEnded, handlers: playbackHandlers } = useMediaPlayback(audioRef, { onEnded, onProgressSample, startAtSeconds, endOverlay }, playbackControlsRef, url);
|
|
16
|
+
const { hasEnded, handlers: playbackHandlers, attachElement } = useMediaPlayback(audioRef, { onEnded, onProgressSample, startAtSeconds, endOverlay }, playbackControlsRef, url);
|
|
17
|
+
// The element attaches through the playback lifecycle so the hook knows the
|
|
18
|
+
// active generation; detach (unmount) retires it.
|
|
19
|
+
const setAudioRef = React.useCallback((el) => {
|
|
20
|
+
audioRef.current = el;
|
|
21
|
+
attachElement(el);
|
|
22
|
+
}, [attachElement]);
|
|
17
23
|
const { ref: inViewRef, inView } = useInView({
|
|
18
24
|
triggerOnce: false,
|
|
19
25
|
threshold: 0.01,
|
|
@@ -67,7 +73,7 @@ export function AudioViewer(props) {
|
|
|
67
73
|
return _jsx(_Fragment, { children: fallback });
|
|
68
74
|
return (_jsx("div", { className: className, children: _jsx("div", { className: "mv-audio-error", children: _jsx("p", { children: "Failed to load audio" }) }) }));
|
|
69
75
|
}
|
|
70
|
-
return (_jsx("div", { ref: inViewRef, className: className, style: { position: "relative", width: "100%", outline: "none" }, children: !shouldLoad ? (showSkeleton ? _jsx(Skeleton, { style: { height: "3.5rem", width: "100%" } }) : null) : (_jsxs(_Fragment, { children: [showSkeleton && !isLoaded && (_jsx(Skeleton, { style: { position: "absolute", inset: 0, height: "3.5rem", width: "100%" } })), _jsx("audio", { ref:
|
|
76
|
+
return (_jsx("div", { ref: inViewRef, className: className, style: { position: "relative", width: "100%", outline: "none" }, children: !shouldLoad ? (showSkeleton ? _jsx(Skeleton, { style: { height: "3.5rem", width: "100%" } }) : null) : (_jsxs(_Fragment, { children: [showSkeleton && !isLoaded && (_jsx(Skeleton, { style: { position: "absolute", inset: 0, height: "3.5rem", width: "100%" } })), _jsx("audio", { ref: setAudioRef, src: url, className: mediaClassName, controls: false, autoPlay: autoPlay, loop: loop, preload: preload, onLoadedData: handleLoadedData, onCanPlay: handleCanPlay, onError: handleError, onLoadedMetadata: handleLoadedMetadata, onTimeUpdate: playbackHandlers.onTimeUpdate, onPlay: playbackHandlers.onPlay, onPause: playbackHandlers.onPause, onSeeked: playbackHandlers.onSeeked, onEnded: playbackHandlers.onEnded, style: {
|
|
71
77
|
width: "100%",
|
|
72
78
|
opacity: isLoaded ? 1 : 0,
|
|
73
79
|
transition: "opacity 300ms ease",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-viewer.js","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { AudioViewerProps } from \"../types.js\";\nimport { useMediaPlayback } from \"./use-media-playback.js\";\nimport { AudioPlayerChrome } from \"./audio-player-chrome.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function AudioViewer(props: AudioViewerProps) {\n const {\n url,\n className,\n mediaClassName,\n autoPlay,\n loop = false,\n lazy = true,\n skeleton: showSkeleton = true,\n priority = false,\n preload = \"metadata\",\n onLoad,\n onLoadChange,\n onError,\n fallback,\n loadTimeoutMs,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n visualizerMode,\n persistKey,\n extraActions,\n } = props;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [hasMetadata, setHasMetadata] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const audioRef = React.useRef<HTMLAudioElement>(null);\n\n // Additive playback API — derives from element events (no observer, MEDIA-102).\n const { hasEnded, handlers: playbackHandlers } = useMediaPlayback(\n audioRef,\n { onEnded, onProgressSample, startAtSeconds, endOverlay },\n playbackControlsRef,\n url,\n );\n\n const { ref: inViewRef, inView } = useInView({\n triggerOnce: false,\n threshold: 0.01,\n skip: priority || shouldLoad,\n rootMargin: \"100px\",\n });\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n }, [inView, shouldLoad]);\n\n // Accordion compatibility: delayed auto-load on mount\n React.useEffect(() => {\n if (priority || shouldLoad) return;\n const timer = setTimeout(() => {\n if (!shouldLoad) setShouldLoad(true);\n }, 100);\n return () => clearTimeout(timer);\n }, [url, priority, shouldLoad]);\n\n React.useEffect(() => {\n onLoadChange?.(!isLoaded);\n }, [isLoaded, onLoadChange]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n setHasMetadata(false);\n }, [url]);\n\n const handleLoadedData = React.useCallback(() => {\n setIsLoaded(true);\n onLoad?.();\n }, [onLoad]);\n\n const handleCanPlay = React.useCallback(() => {\n setIsLoaded(true);\n }, []);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n setIsLoaded(true); // Stop skeleton on error\n onError?.();\n }, [onError]);\n\n // Watchdog settles on METADATA — preload=\"metadata\" audio may not fire\n // loadeddata/canplay until play, and must not false-positive.\n const handleLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLAudioElement>) => {\n setHasMetadata(true);\n playbackHandlers.onLoadedMetadata(e);\n },\n [playbackHandlers]\n );\n\n useLoadWatchdog(\n shouldLoad && !isLoaded && !hasMetadata && !hasError,\n loadTimeoutMs,\n handleError\n );\n\n if (hasError) {\n if (fallback) return <>{fallback}</>;\n return (\n <div className={className}>\n <div className=\"mv-audio-error\">\n <p>Failed to load audio</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n ref={inViewRef}\n className={className}\n style={{ position: \"relative\", width: \"100%\", outline: \"none\" }}\n >\n {!shouldLoad ? (\n showSkeleton ? <Skeleton style={{ height: \"3.5rem\", width: \"100%\" }} /> : null\n ) : (\n <>\n {showSkeleton && !isLoaded && (\n <Skeleton style={{ position: \"absolute\", inset: 0, height: \"3.5rem\", width: \"100%\" }} />\n )}\n <audio\n ref={audioRef}\n src={url}\n className={mediaClassName}\n controls={false}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n onLoadedData={handleLoadedData}\n onCanPlay={handleCanPlay}\n onError={handleError}\n onLoadedMetadata={handleLoadedMetadata}\n onTimeUpdate={playbackHandlers.onTimeUpdate}\n onPlay={playbackHandlers.onPlay}\n onPause={playbackHandlers.onPause}\n onSeeked={playbackHandlers.onSeeked}\n onEnded={playbackHandlers.onEnded}\n style={{\n width: \"100%\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity 300ms ease\",\n }}\n />\n <AudioPlayerChrome\n audioRef={audioRef}\n visualizerMode={visualizerMode}\n persistKey={persistKey}\n extraActions={extraActions}\n />\n {endOverlay != null && hasEnded && (\n <div\n className=\"mv-end-overlay\"\n style={{ position: \"absolute\", inset: 0, zIndex: 2 }}\n >\n {endOverlay}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audio-viewer.js","sourceRoot":"","sources":["../../src/react/audio-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { AudioViewerProps } from \"../types.js\";\nimport { useMediaPlayback } from \"./use-media-playback.js\";\nimport { AudioPlayerChrome } from \"./audio-player-chrome.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function AudioViewer(props: AudioViewerProps) {\n const {\n url,\n className,\n mediaClassName,\n autoPlay,\n loop = false,\n lazy = true,\n skeleton: showSkeleton = true,\n priority = false,\n preload = \"metadata\",\n onLoad,\n onLoadChange,\n onError,\n fallback,\n loadTimeoutMs,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n visualizerMode,\n persistKey,\n extraActions,\n } = props;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [hasMetadata, setHasMetadata] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const audioRef = React.useRef<HTMLAudioElement>(null);\n\n // Additive playback API — derives from element events (no observer, MEDIA-102).\n const { hasEnded, handlers: playbackHandlers, attachElement } = useMediaPlayback(\n audioRef,\n { onEnded, onProgressSample, startAtSeconds, endOverlay },\n playbackControlsRef,\n url,\n );\n\n // The element attaches through the playback lifecycle so the hook knows the\n // active generation; detach (unmount) retires it.\n const setAudioRef = React.useCallback(\n (el: HTMLAudioElement | null) => {\n audioRef.current = el;\n attachElement(el);\n },\n [attachElement],\n );\n\n const { ref: inViewRef, inView } = useInView({\n triggerOnce: false,\n threshold: 0.01,\n skip: priority || shouldLoad,\n rootMargin: \"100px\",\n });\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n }, [inView, shouldLoad]);\n\n // Accordion compatibility: delayed auto-load on mount\n React.useEffect(() => {\n if (priority || shouldLoad) return;\n const timer = setTimeout(() => {\n if (!shouldLoad) setShouldLoad(true);\n }, 100);\n return () => clearTimeout(timer);\n }, [url, priority, shouldLoad]);\n\n React.useEffect(() => {\n onLoadChange?.(!isLoaded);\n }, [isLoaded, onLoadChange]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n setHasMetadata(false);\n }, [url]);\n\n const handleLoadedData = React.useCallback(() => {\n setIsLoaded(true);\n onLoad?.();\n }, [onLoad]);\n\n const handleCanPlay = React.useCallback(() => {\n setIsLoaded(true);\n }, []);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n setIsLoaded(true); // Stop skeleton on error\n onError?.();\n }, [onError]);\n\n // Watchdog settles on METADATA — preload=\"metadata\" audio may not fire\n // loadeddata/canplay until play, and must not false-positive.\n const handleLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLAudioElement>) => {\n setHasMetadata(true);\n playbackHandlers.onLoadedMetadata(e);\n },\n [playbackHandlers]\n );\n\n useLoadWatchdog(\n shouldLoad && !isLoaded && !hasMetadata && !hasError,\n loadTimeoutMs,\n handleError\n );\n\n if (hasError) {\n if (fallback) return <>{fallback}</>;\n return (\n <div className={className}>\n <div className=\"mv-audio-error\">\n <p>Failed to load audio</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n ref={inViewRef}\n className={className}\n style={{ position: \"relative\", width: \"100%\", outline: \"none\" }}\n >\n {!shouldLoad ? (\n showSkeleton ? <Skeleton style={{ height: \"3.5rem\", width: \"100%\" }} /> : null\n ) : (\n <>\n {showSkeleton && !isLoaded && (\n <Skeleton style={{ position: \"absolute\", inset: 0, height: \"3.5rem\", width: \"100%\" }} />\n )}\n <audio\n ref={setAudioRef}\n src={url}\n className={mediaClassName}\n controls={false}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n onLoadedData={handleLoadedData}\n onCanPlay={handleCanPlay}\n onError={handleError}\n onLoadedMetadata={handleLoadedMetadata}\n onTimeUpdate={playbackHandlers.onTimeUpdate}\n onPlay={playbackHandlers.onPlay}\n onPause={playbackHandlers.onPause}\n onSeeked={playbackHandlers.onSeeked}\n onEnded={playbackHandlers.onEnded}\n style={{\n width: \"100%\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity 300ms ease\",\n }}\n />\n <AudioPlayerChrome\n audioRef={audioRef}\n visualizerMode={visualizerMode}\n persistKey={persistKey}\n extraActions={extraActions}\n />\n {endOverlay != null && hasEnded && (\n <div\n className=\"mv-end-overlay\"\n style={{ position: \"absolute\", inset: 0, zIndex: 2 }}\n >\n {endOverlay}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-viewer.d.ts","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,iBAAiB,EAAiC,MAAM,aAAa,CAAC;AA6GpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"media-viewer.d.ts","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,iBAAiB,EAAiC,MAAM,aAAa,CAAC;AA6GpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,4BA2OnD;AAED,wBAAwB;AACxB,eAAO,MAAM,YAAY,oBAAc,CAAC"}
|
|
@@ -59,7 +59,7 @@ function RecoveryOverlay({ state, onManualRetry, isCircular }) {
|
|
|
59
59
|
// Main component
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
export function MediaViewer(props) {
|
|
62
|
-
const { url, alt, type, className, unloadOnExit, isCircular = false, priority, skeleton, lazy, controls, autoPlay, muted, loop, posterUrl, preload, autoPlayOnVisible, onLoad, onError, onLoadChange, onEnded, onProgressSample, startAtSeconds, endOverlay, playbackControlsRef, audioVisualizerMode, audioPersistKey, audioExtraActions, fallbackMode = "link", fallbackLabel, filename, mime, name, recoveryAdapter, assetStatusHint, loadTimeoutMs, } = props;
|
|
62
|
+
const { url, alt, type, className, unloadOnExit, isCircular = false, enableZoom, priority, skeleton, lazy, controls, autoPlay, muted, loop, posterUrl, preload, autoPlayOnVisible, onLoad, onError, onLoadChange, onEnded, onProgressSample, startAtSeconds, endOverlay, playbackControlsRef, audioVisualizerMode, audioPersistKey, audioExtraActions, fallbackMode = "link", fallbackLabel, filename, mime, name, recoveryAdapter, assetStatusHint, loadTimeoutMs, } = props;
|
|
63
63
|
const [hasError, setHasError] = React.useState(false);
|
|
64
64
|
const [sourceUrl, setSourceUrl] = React.useState(null);
|
|
65
65
|
// Remount key: incrementing this causes child media elements to unmount and
|
|
@@ -138,7 +138,7 @@ export function MediaViewer(props) {
|
|
|
138
138
|
}
|
|
139
139
|
const t = inferType({ type, mime, name, url: sourceUrl });
|
|
140
140
|
if (t === "image") {
|
|
141
|
-
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(ImageViewer, { url: sourceUrl, alt: alt, lazy: lazy, priority: priority, skeleton: skeleton, unloadOnExit: unloadOnExit, isCircular: isCircular, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onError: handleError, fallback: _jsx(ErrorFallback, { isCircular: isCircular }) }, remountKey) }));
|
|
141
|
+
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(ImageViewer, { url: sourceUrl, alt: alt, lazy: lazy, priority: priority, skeleton: skeleton, unloadOnExit: unloadOnExit, isCircular: isCircular, enableZoom: enableZoom, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onError: handleError, fallback: _jsx(ErrorFallback, { isCircular: isCircular }) }, remountKey) }));
|
|
142
142
|
}
|
|
143
143
|
if (t === "video") {
|
|
144
144
|
return (_jsx("div", { className: className, style: wrapperStyle, children: _jsx(VideoViewer, { url: sourceUrl, lazy: lazy, priority: priority, skeleton: skeleton, unloadOnExit: unloadOnExit, controls: controls, autoPlay: autoPlay, muted: muted, loop: loop, posterUrl: posterUrl, preload: preload, autoPlayOnVisible: autoPlayOnVisible, loadTimeoutMs: loadTimeoutMs, onLoad: handleLoad, onError: handleError, onEnded: onEnded, onProgressSample: onProgressSample, startAtSeconds: startAtSeconds, endOverlay: endOverlay, playbackControlsRef: playbackControlsRef }, remountKey) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-viewer.js","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { getSimplifiedMediaType } from \"@ttt-productions/media-schemas\";\nimport type { SimplifiedMediaType } from \"@ttt-productions/media-schemas\";\n\nimport type { MediaPreviewProps, MediaViewerType, FallbackMode } from \"../types.js\";\nimport { MediaFallbackLink, shouldShowFallback, EmptyFallback, ErrorFallback } from \"./fallback.js\";\nimport { ImageViewer } from \"./image-viewer.js\";\nimport { VideoViewer } from \"./video-viewer.js\";\nimport { AudioViewer } from \"./audio-viewer.js\";\nimport { useMediaRecovery } from \"./use-media-recovery.js\";\nimport type { RecoveryState } from \"../recovery.js\";\n\nfunction inferType(props: { type?: MediaPreviewProps[\"type\"]; mime?: string; name?: string; url?: string }): MediaViewerType {\n if (props.type) {\n // Handle MIME strings like \"image/png\" -> \"image\"\n const primary = typeof props.type === \"string\" ? props.type.split(\"/\")[0] : props.type;\n if (primary === \"image\" || primary === \"video\" || primary === \"audio\") return primary;\n if (primary === \"other\") return \"other\";\n }\n\n const hint = props.mime ?? props.name ?? props.url;\n if (!hint) return \"other\";\n const simplified: SimplifiedMediaType = getSimplifiedMediaType(hint);\n return simplified;\n}\n\n// ---------------------------------------------------------------------------\n// Recovery-phase UI helpers\n// ---------------------------------------------------------------------------\n\nfunction RecoveryOverlay({ state, onManualRetry, isCircular }: {\n state: RecoveryState;\n onManualRetry: () => void;\n isCircular?: boolean;\n}) {\n const wrapStyle: React.CSSProperties = {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n gap: \"0.5rem\",\n ...(isCircular ? { borderRadius: \"50%\" } : {}),\n };\n\n if (state.phase === \"processing\") {\n return (\n <div className=\"mv-recovery-processing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Processing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"finalizing\") {\n return (\n <div className=\"mv-recovery-finalizing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Finishing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"transient-retry\" || state.phase === \"propagating\") {\n return (\n <div className=\"mv-recovery-retrying\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Loading…</span>\n </div>\n );\n }\n\n if (state.phase === \"auth-retry\") {\n return (\n <div className=\"mv-recovery-auth\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Refreshing access…</span>\n </div>\n );\n }\n\n if (state.phase === \"hard-unavailable\") {\n return (\n <div className=\"mv-recovery-hard\" style={wrapStyle}>\n <ErrorFallback isCircular={isCircular} />\n {state.reason === \"auth\" ? (\n <span className=\"mv-recovery-label\">Access denied.</span>\n ) : (\n <span className=\"mv-recovery-label\">Media unavailable.</span>\n )}\n </div>\n );\n }\n\n if (state.phase === \"max-wait-fallback\") {\n return (\n <div className=\"mv-recovery-max-wait\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">This media is taking longer than expected.</span>\n <button\n type=\"button\"\n className=\"mv-recovery-retry-btn\"\n onClick={onManualRetry}\n >\n Retry\n </button>\n </div>\n );\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function MediaViewer(props: MediaPreviewProps) {\n const {\n url,\n alt,\n type,\n className,\n unloadOnExit,\n isCircular = false,\n priority,\n skeleton,\n lazy,\n controls,\n autoPlay,\n muted,\n loop,\n posterUrl,\n preload,\n autoPlayOnVisible,\n onLoad,\n onError,\n onLoadChange,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n audioVisualizerMode,\n audioPersistKey,\n audioExtraActions,\n fallbackMode = \"link\",\n fallbackLabel,\n filename,\n mime,\n name,\n recoveryAdapter,\n assetStatusHint,\n loadTimeoutMs,\n } = props;\n\n const [hasError, setHasError] = React.useState(false);\n const [sourceUrl, setSourceUrl] = React.useState<string | null>(null);\n\n // Remount key: incrementing this causes child media elements to unmount and\n // re-mount from scratch, re-issuing the HTTP request.\n const [remountKey, setRemountKey] = React.useState(0);\n\n const handleRemount = React.useCallback(() => {\n setHasError(false);\n setRemountKey((k) => k + 1);\n }, []);\n\n // Recovery hook (no-op when recoveryAdapter is omitted)\n const { recoveryState, onMediaError, onMediaLoad, manualRetry } =\n useMediaRecovery({\n url: typeof url === \"string\" ? url : null,\n adapter: recoveryAdapter,\n statusHint: assetStatusHint,\n onRemount: handleRemount,\n });\n\n React.useEffect(() => {\n setHasError(false);\n let objectUrl: string | null = null;\n\n if (typeof url === \"string\") {\n setSourceUrl(url);\n } else if (url instanceof Blob) {\n objectUrl = URL.createObjectURL(url);\n setSourceUrl(objectUrl);\n } else {\n setSourceUrl(null);\n }\n\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [url]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n // If a recovery adapter is provided, delegate to the recovery hook.\n if (recoveryAdapter) {\n onMediaError();\n }\n }, [onError, recoveryAdapter, onMediaError]);\n\n const handleLoad = React.useCallback(() => {\n setHasError(false);\n onLoad?.();\n // Signal successful load to the recovery state machine.\n if (recoveryAdapter) {\n onMediaLoad();\n }\n }, [onLoad, recoveryAdapter, onMediaLoad]);\n\n const circularStyle: React.CSSProperties = isCircular\n ? { borderRadius: \"50%\", overflow: \"hidden\" }\n : {};\n\n const wrapperStyle: React.CSSProperties = {\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n ...circularStyle,\n };\n\n // When recovery is active and in a terminal/blocking state, show the overlay.\n const isInRecovery =\n recoveryAdapter !== undefined &&\n recoveryState.phase !== \"idle\" &&\n recoveryState.phase !== \"loaded\" &&\n recoveryState.phase !== \"loading\";\n\n // No URL -> empty fallback\n if (!sourceUrl) {\n return (\n <div className={className} style={wrapperStyle}>\n <EmptyFallback isCircular={isCircular} />\n </div>\n );\n }\n\n // Recovery overlay (for all non-idle/non-loaded recovery states)\n if (isInRecovery && hasError) {\n return (\n <div className={className} style={wrapperStyle}>\n <RecoveryOverlay\n state={recoveryState}\n onManualRetry={manualRetry}\n isCircular={isCircular}\n />\n </div>\n );\n }\n\n // Vanilla error (no recovery adapter) -> error fallback\n if (hasError && !recoveryAdapter) {\n return (\n <div className={className} style={wrapperStyle}>\n <ErrorFallback isCircular={isCircular} />\n </div>\n );\n }\n\n const t = inferType({ type, mime, name, url: sourceUrl });\n\n if (t === \"image\") {\n return (\n <div className={className} style={wrapperStyle}>\n <ImageViewer\n key={remountKey}\n url={sourceUrl}\n alt={alt}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n isCircular={isCircular}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n fallback={<ErrorFallback isCircular={isCircular} />}\n />\n </div>\n );\n }\n\n if (t === \"video\") {\n return (\n <div className={className} style={wrapperStyle}>\n <VideoViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n controls={controls}\n autoPlay={autoPlay}\n muted={muted}\n loop={loop}\n posterUrl={posterUrl}\n preload={preload}\n autoPlayOnVisible={autoPlayOnVisible}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n if (t === \"audio\") {\n return (\n <div className={className} style={wrapperStyle}>\n <AudioViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n visualizerMode={audioVisualizerMode}\n persistKey={audioPersistKey}\n extraActions={audioExtraActions}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onLoadChange={onLoadChange}\n onError={handleError}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n // Unknown type -> fallback link\n return shouldShowFallback(fallbackMode as FallbackMode) ? (\n <div className={className} style={wrapperStyle}>\n <MediaFallbackLink url={sourceUrl} filename={filename} label={fallbackLabel ?? \"Download\"} />\n </div>\n ) : null;\n}\n\n/** Back-compat alias */\nexport const MediaPreview = MediaViewer;\n"]}
|
|
1
|
+
{"version":3,"file":"media-viewer.js","sourceRoot":"","sources":["../../src/react/media-viewer.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAIxE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,SAAS,SAAS,CAAC,KAAuF;IACxG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,kDAAkD;QAClD,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACvF,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,OAAO;YAAE,OAAO,OAAO,CAAC;QACtF,IAAI,OAAO,KAAK,OAAO;YAAE,OAAO,OAAO,CAAC;IAC1C,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC;IACnD,IAAI,CAAC,IAAI;QAAE,OAAO,OAAO,CAAC;IAC1B,MAAM,UAAU,GAAwB,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACrE,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,8EAA8E;AAC9E,4BAA4B;AAC5B,8EAA8E;AAE9E,SAAS,eAAe,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAI1D;IACC,MAAM,SAAS,GAAwB;QACrC,OAAO,EAAE,MAAM;QACf,aAAa,EAAE,QAAQ;QACvB,UAAU,EAAE,QAAQ;QACpB,cAAc,EAAE,QAAQ;QACxB,KAAK,EAAE,MAAM;QACb,MAAM,EAAE,MAAM;QACd,GAAG,EAAE,QAAQ;QACb,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/C,CAAC;IAEF,IAAI,KAAK,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;QACjC,OAAO,CACL,cAAK,SAAS,EAAC,wBAAwB,EAAC,KAAK,EAAE,SAAS,YACtD,eAAM,SAAS,EAAC,mBAAmB,uCAAgC,GAC/D,CACP,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;QACjC,OAAO,CACL,cAAK,SAAS,EAAC,wBAAwB,EAAC,KAAK,EAAE,SAAS,YACtD,eAAM,SAAS,EAAC,mBAAmB,sCAA+B,GAC9D,CACP,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,iBAAiB,IAAI,KAAK,CAAC,KAAK,KAAK,aAAa,EAAE,CAAC;QACvE,OAAO,CACL,cAAK,SAAS,EAAC,sBAAsB,EAAC,KAAK,EAAE,SAAS,YACpD,eAAM,SAAS,EAAC,mBAAmB,8BAAuB,GACtD,CACP,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;QACjC,OAAO,CACL,cAAK,SAAS,EAAC,kBAAkB,EAAC,KAAK,EAAE,SAAS,YAChD,eAAM,SAAS,EAAC,mBAAmB,wCAAiC,GAChE,CACP,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,kBAAkB,EAAE,CAAC;QACvC,OAAO,CACL,eAAK,SAAS,EAAC,kBAAkB,EAAC,KAAK,EAAE,SAAS,aAChD,KAAC,aAAa,IAAC,UAAU,EAAE,UAAU,GAAI,EACxC,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,CACzB,eAAM,SAAS,EAAC,mBAAmB,+BAAsB,CAC1D,CAAC,CAAC,CAAC,CACF,eAAM,SAAS,EAAC,mBAAmB,mCAA0B,CAC9D,IACG,CACP,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,mBAAmB,EAAE,CAAC;QACxC,OAAO,CACL,eAAK,SAAS,EAAC,sBAAsB,EAAC,KAAK,EAAE,SAAS,aACpD,eAAM,SAAS,EAAC,mBAAmB,2DAAkD,EACrF,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,uBAAuB,EACjC,OAAO,EAAE,aAAa,sBAGf,IACL,CACP,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,MAAM,UAAU,WAAW,CAAC,KAAwB;IAClD,MAAM,EACJ,GAAG,EACH,GAAG,EACH,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,UAAU,GAAG,KAAK,EAClB,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,IAAI,EACJ,SAAS,EACT,OAAO,EACP,iBAAiB,EACjB,MAAM,EACN,OAAO,EACP,YAAY,EACZ,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,YAAY,GAAG,MAAM,EACrB,aAAa,EACb,QAAQ,EACR,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,eAAe,EACf,aAAa,GACd,GAAG,KAAK,CAAC;IAEV,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAEtE,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEtD,MAAM,aAAa,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QAC3C,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,aAAa,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,wDAAwD;IACxD,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,GAC7D,gBAAgB,CAAC;QACf,GAAG,EAAE,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;QACzC,OAAO,EAAE,eAAe;QACxB,UAAU,EAAE,eAAe;QAC3B,SAAS,EAAE,aAAa;KACzB,CAAC,CAAC;IAEL,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,IAAI,SAAS,GAAkB,IAAI,CAAC;QAEpC,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YAC5B,YAAY,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;aAAM,IAAI,GAAG,YAAY,IAAI,EAAE,CAAC;YAC/B,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YACrC,YAAY,CAAC,SAAS,CAAC,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QAED,OAAO,GAAG,EAAE;YACV,IAAI,SAAS;gBAAE,GAAG,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QAChD,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEV,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QACzC,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO,EAAE,EAAE,CAAC;QACZ,oEAAoE;QACpE,IAAI,eAAe,EAAE,CAAC;YACpB,YAAY,EAAE,CAAC;QACjB,CAAC;IACH,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC;IAE7C,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QACxC,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,MAAM,EAAE,EAAE,CAAC;QACX,wDAAwD;QACxD,IAAI,eAAe,EAAE,CAAC;YACpB,WAAW,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC,CAAC;IAE3C,MAAM,aAAa,GAAwB,UAAU;QACnD,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAC7C,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,YAAY,GAAwB;QACxC,QAAQ,EAAE,UAAU;QACpB,KAAK,EAAE,MAAM;QACb,MAAM,EAAE,MAAM;QACd,GAAG,aAAa;KACjB,CAAC;IAEF,8EAA8E;IAC9E,MAAM,YAAY,GAChB,eAAe,KAAK,SAAS;QAC7B,aAAa,CAAC,KAAK,KAAK,MAAM;QAC9B,aAAa,CAAC,KAAK,KAAK,QAAQ;QAChC,aAAa,CAAC,KAAK,KAAK,SAAS,CAAC;IAEpC,2BAA2B;IAC3B,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CACL,cAAK,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,YAC5C,KAAC,aAAa,IAAC,UAAU,EAAE,UAAU,GAAI,GACrC,CACP,CAAC;IACJ,CAAC;IAED,iEAAiE;IACjE,IAAI,YAAY,IAAI,QAAQ,EAAE,CAAC;QAC7B,OAAO,CACL,cAAK,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,YAC5C,KAAC,eAAe,IACd,KAAK,EAAE,aAAa,EACpB,aAAa,EAAE,WAAW,EAC1B,UAAU,EAAE,UAAU,GACtB,GACE,CACP,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,IAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,CAAC;QACjC,OAAO,CACL,cAAK,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,YAC5C,KAAC,aAAa,IAAC,UAAU,EAAE,UAAU,GAAI,GACrC,CACP,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IAE1D,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;QAClB,OAAO,CACL,cAAK,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,YAC5C,KAAC,WAAW,IAEV,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,UAAU,EACtB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,KAAC,aAAa,IAAC,UAAU,EAAE,UAAU,GAAI,IAZ9C,UAAU,CAaf,GACE,CACP,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;QAClB,OAAO,CACL,cAAK,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,YAC5C,KAAC,WAAW,IAEV,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,iBAAiB,EACpC,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,OAAO,EAChB,gBAAgB,EAAE,gBAAgB,EAClC,cAAc,EAAE,cAAc,EAC9B,UAAU,EAAE,UAAU,EACtB,mBAAmB,EAAE,mBAAmB,IApBnC,UAAU,CAqBf,GACE,CACP,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;QAClB,OAAO,CACL,cAAK,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,YAC5C,KAAC,WAAW,IAEV,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,mBAAmB,EACnC,UAAU,EAAE,eAAe,EAC3B,YAAY,EAAE,iBAAiB,EAC/B,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,OAAO,EAChB,gBAAgB,EAAE,gBAAgB,EAClC,cAAc,EAAE,cAAc,EAC9B,UAAU,EAAE,UAAU,EACtB,mBAAmB,EAAE,mBAAmB,IAnBnC,UAAU,CAoBf,GACE,CACP,CAAC;IACJ,CAAC;IAED,gCAAgC;IAChC,OAAO,kBAAkB,CAAC,YAA4B,CAAC,CAAC,CAAC,CAAC,CACxD,cAAK,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,YAC5C,KAAC,iBAAiB,IAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,IAAI,UAAU,GAAI,GACzF,CACP,CAAC,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,wBAAwB;AACxB,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC","sourcesContent":["import * as React from \"react\";\nimport { getSimplifiedMediaType } from \"@ttt-productions/media-schemas\";\nimport type { SimplifiedMediaType } from \"@ttt-productions/media-schemas\";\n\nimport type { MediaPreviewProps, MediaViewerType, FallbackMode } from \"../types.js\";\nimport { MediaFallbackLink, shouldShowFallback, EmptyFallback, ErrorFallback } from \"./fallback.js\";\nimport { ImageViewer } from \"./image-viewer.js\";\nimport { VideoViewer } from \"./video-viewer.js\";\nimport { AudioViewer } from \"./audio-viewer.js\";\nimport { useMediaRecovery } from \"./use-media-recovery.js\";\nimport type { RecoveryState } from \"../recovery.js\";\n\nfunction inferType(props: { type?: MediaPreviewProps[\"type\"]; mime?: string; name?: string; url?: string }): MediaViewerType {\n if (props.type) {\n // Handle MIME strings like \"image/png\" -> \"image\"\n const primary = typeof props.type === \"string\" ? props.type.split(\"/\")[0] : props.type;\n if (primary === \"image\" || primary === \"video\" || primary === \"audio\") return primary;\n if (primary === \"other\") return \"other\";\n }\n\n const hint = props.mime ?? props.name ?? props.url;\n if (!hint) return \"other\";\n const simplified: SimplifiedMediaType = getSimplifiedMediaType(hint);\n return simplified;\n}\n\n// ---------------------------------------------------------------------------\n// Recovery-phase UI helpers\n// ---------------------------------------------------------------------------\n\nfunction RecoveryOverlay({ state, onManualRetry, isCircular }: {\n state: RecoveryState;\n onManualRetry: () => void;\n isCircular?: boolean;\n}) {\n const wrapStyle: React.CSSProperties = {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n gap: \"0.5rem\",\n ...(isCircular ? { borderRadius: \"50%\" } : {}),\n };\n\n if (state.phase === \"processing\") {\n return (\n <div className=\"mv-recovery-processing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Processing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"finalizing\") {\n return (\n <div className=\"mv-recovery-finalizing\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Finishing media…</span>\n </div>\n );\n }\n\n if (state.phase === \"transient-retry\" || state.phase === \"propagating\") {\n return (\n <div className=\"mv-recovery-retrying\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Loading…</span>\n </div>\n );\n }\n\n if (state.phase === \"auth-retry\") {\n return (\n <div className=\"mv-recovery-auth\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">Refreshing access…</span>\n </div>\n );\n }\n\n if (state.phase === \"hard-unavailable\") {\n return (\n <div className=\"mv-recovery-hard\" style={wrapStyle}>\n <ErrorFallback isCircular={isCircular} />\n {state.reason === \"auth\" ? (\n <span className=\"mv-recovery-label\">Access denied.</span>\n ) : (\n <span className=\"mv-recovery-label\">Media unavailable.</span>\n )}\n </div>\n );\n }\n\n if (state.phase === \"max-wait-fallback\") {\n return (\n <div className=\"mv-recovery-max-wait\" style={wrapStyle}>\n <span className=\"mv-recovery-label\">This media is taking longer than expected.</span>\n <button\n type=\"button\"\n className=\"mv-recovery-retry-btn\"\n onClick={onManualRetry}\n >\n Retry\n </button>\n </div>\n );\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function MediaViewer(props: MediaPreviewProps) {\n const {\n url,\n alt,\n type,\n className,\n unloadOnExit,\n isCircular = false,\n enableZoom,\n priority,\n skeleton,\n lazy,\n controls,\n autoPlay,\n muted,\n loop,\n posterUrl,\n preload,\n autoPlayOnVisible,\n onLoad,\n onError,\n onLoadChange,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n audioVisualizerMode,\n audioPersistKey,\n audioExtraActions,\n fallbackMode = \"link\",\n fallbackLabel,\n filename,\n mime,\n name,\n recoveryAdapter,\n assetStatusHint,\n loadTimeoutMs,\n } = props;\n\n const [hasError, setHasError] = React.useState(false);\n const [sourceUrl, setSourceUrl] = React.useState<string | null>(null);\n\n // Remount key: incrementing this causes child media elements to unmount and\n // re-mount from scratch, re-issuing the HTTP request.\n const [remountKey, setRemountKey] = React.useState(0);\n\n const handleRemount = React.useCallback(() => {\n setHasError(false);\n setRemountKey((k) => k + 1);\n }, []);\n\n // Recovery hook (no-op when recoveryAdapter is omitted)\n const { recoveryState, onMediaError, onMediaLoad, manualRetry } =\n useMediaRecovery({\n url: typeof url === \"string\" ? url : null,\n adapter: recoveryAdapter,\n statusHint: assetStatusHint,\n onRemount: handleRemount,\n });\n\n React.useEffect(() => {\n setHasError(false);\n let objectUrl: string | null = null;\n\n if (typeof url === \"string\") {\n setSourceUrl(url);\n } else if (url instanceof Blob) {\n objectUrl = URL.createObjectURL(url);\n setSourceUrl(objectUrl);\n } else {\n setSourceUrl(null);\n }\n\n return () => {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [url]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n // If a recovery adapter is provided, delegate to the recovery hook.\n if (recoveryAdapter) {\n onMediaError();\n }\n }, [onError, recoveryAdapter, onMediaError]);\n\n const handleLoad = React.useCallback(() => {\n setHasError(false);\n onLoad?.();\n // Signal successful load to the recovery state machine.\n if (recoveryAdapter) {\n onMediaLoad();\n }\n }, [onLoad, recoveryAdapter, onMediaLoad]);\n\n const circularStyle: React.CSSProperties = isCircular\n ? { borderRadius: \"50%\", overflow: \"hidden\" }\n : {};\n\n const wrapperStyle: React.CSSProperties = {\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n ...circularStyle,\n };\n\n // When recovery is active and in a terminal/blocking state, show the overlay.\n const isInRecovery =\n recoveryAdapter !== undefined &&\n recoveryState.phase !== \"idle\" &&\n recoveryState.phase !== \"loaded\" &&\n recoveryState.phase !== \"loading\";\n\n // No URL -> empty fallback\n if (!sourceUrl) {\n return (\n <div className={className} style={wrapperStyle}>\n <EmptyFallback isCircular={isCircular} />\n </div>\n );\n }\n\n // Recovery overlay (for all non-idle/non-loaded recovery states)\n if (isInRecovery && hasError) {\n return (\n <div className={className} style={wrapperStyle}>\n <RecoveryOverlay\n state={recoveryState}\n onManualRetry={manualRetry}\n isCircular={isCircular}\n />\n </div>\n );\n }\n\n // Vanilla error (no recovery adapter) -> error fallback\n if (hasError && !recoveryAdapter) {\n return (\n <div className={className} style={wrapperStyle}>\n <ErrorFallback isCircular={isCircular} />\n </div>\n );\n }\n\n const t = inferType({ type, mime, name, url: sourceUrl });\n\n if (t === \"image\") {\n return (\n <div className={className} style={wrapperStyle}>\n <ImageViewer\n key={remountKey}\n url={sourceUrl}\n alt={alt}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n isCircular={isCircular}\n enableZoom={enableZoom}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n fallback={<ErrorFallback isCircular={isCircular} />}\n />\n </div>\n );\n }\n\n if (t === \"video\") {\n return (\n <div className={className} style={wrapperStyle}>\n <VideoViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n unloadOnExit={unloadOnExit}\n controls={controls}\n autoPlay={autoPlay}\n muted={muted}\n loop={loop}\n posterUrl={posterUrl}\n preload={preload}\n autoPlayOnVisible={autoPlayOnVisible}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onError={handleError}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n if (t === \"audio\") {\n return (\n <div className={className} style={wrapperStyle}>\n <AudioViewer\n key={remountKey}\n url={sourceUrl}\n lazy={lazy}\n priority={priority}\n skeleton={skeleton}\n autoPlay={autoPlay}\n loop={loop}\n preload={preload}\n visualizerMode={audioVisualizerMode}\n persistKey={audioPersistKey}\n extraActions={audioExtraActions}\n loadTimeoutMs={loadTimeoutMs}\n onLoad={handleLoad}\n onLoadChange={onLoadChange}\n onError={handleError}\n onEnded={onEnded}\n onProgressSample={onProgressSample}\n startAtSeconds={startAtSeconds}\n endOverlay={endOverlay}\n playbackControlsRef={playbackControlsRef}\n />\n </div>\n );\n }\n\n // Unknown type -> fallback link\n return shouldShowFallback(fallbackMode as FallbackMode) ? (\n <div className={className} style={wrapperStyle}>\n <MediaFallbackLink url={sourceUrl} filename={filename} label={fallbackLabel ?? \"Download\"} />\n </div>\n ) : null;\n}\n\n/** Back-compat alias */\nexport const MediaPreview = MediaViewer;\n"]}
|
|
@@ -26,6 +26,29 @@ export type UseMediaPlaybackResult = {
|
|
|
26
26
|
onSeeked: (e: React.SyntheticEvent<HTMLMediaElement>) => void;
|
|
27
27
|
onEnded: (e: React.SyntheticEvent<HTMLMediaElement>) => void;
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Element lifecycle boundary. Attach the native element through this callback
|
|
31
|
+
* (compose it into the element `ref`) so the hook knows which element
|
|
32
|
+
* generation is ACTIVE. A detach (`null`) retires the outgoing element:
|
|
33
|
+
* its still-live cursor is committed as one final `pause` sample (unless the
|
|
34
|
+
* last commit already reported that exact position, playback never started,
|
|
35
|
+
* or the position is still inside the pre-seed suppression window), and every
|
|
36
|
+
* later `pause`/`seeked`/`ended`/`timeupdate` event from that retired element
|
|
37
|
+
* is ignored — a teardown-generated reset can never masquerade as a user
|
|
38
|
+
* playback event. Opt-in: a consumer that never calls this keeps the previous
|
|
39
|
+
* ungated behavior.
|
|
40
|
+
*/
|
|
41
|
+
attachElement: (element: HTMLMediaElement | null) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Retire the currently attached element NOW, before a destructive media
|
|
44
|
+
* operation the caller is about to perform on it (`pause()` +
|
|
45
|
+
* `removeAttribute("src")` + `load()`, DOM removal, source replacement).
|
|
46
|
+
* Emits the same final-snapshot commit as a detach and stops all further
|
|
47
|
+
* reporting from that element. Idempotent — a later detach of an already
|
|
48
|
+
* retired element emits nothing. Re-attaching an element through
|
|
49
|
+
* `attachElement` reactivates reporting.
|
|
50
|
+
*/
|
|
51
|
+
retireElement: () => void;
|
|
29
52
|
};
|
|
30
53
|
/**
|
|
31
54
|
* Shared playback API for VideoViewer and AudioViewer. Owns the additive
|
|
@@ -39,6 +62,16 @@ export type UseMediaPlaybackResult = {
|
|
|
39
62
|
* is tracked in a component-scoped ref that outlives the element, so playback
|
|
40
63
|
* resumes from where it left off — never from `startAtSeconds` again and never
|
|
41
64
|
* from 0.
|
|
65
|
+
*
|
|
66
|
+
* The hook is also the single owner of the ACTIVE element generation: viewers
|
|
67
|
+
* attach their native element through `attachElement` (composed into the
|
|
68
|
+
* element ref) and call `retireElement` before performing a destructive media
|
|
69
|
+
* operation (`pause()` + src removal + `load()`, source replacement, DOM
|
|
70
|
+
* removal). A retiring element's still-live cursor is committed once as a
|
|
71
|
+
* final `pause` sample, and every media event a torn-down element emits
|
|
72
|
+
* afterwards (browsers reset the cursor to 0 and can fire pause/seeked/
|
|
73
|
+
* timeupdate during teardown) is ignored — so a teardown reset can never be
|
|
74
|
+
* presented to the app as a user playback event.
|
|
42
75
|
*/
|
|
43
76
|
export declare function useMediaPlayback(elementRef: React.RefObject<HTMLMediaElement | null>, props: MediaPlaybackProps, playbackControlsRef?: React.Ref<MediaPlaybackControls>,
|
|
44
77
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-media-playback.d.ts","sourceRoot":"","sources":["../../src/react/use-media-playback.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,qBAAqB,EACrB,kBAAkB,EAEnB,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,MAAM,MAAM,sBAAsB,GAAG;IACnC,4DAA4D;IAC5D,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,QAAQ,EAAE;QACR,gBAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QACtE,YAAY,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAClE,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAC5D,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAC7D,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAC9D,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;KAC9D,CAAC;
|
|
1
|
+
{"version":3,"file":"use-media-playback.d.ts","sourceRoot":"","sources":["../../src/react/use-media-playback.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,qBAAqB,EACrB,kBAAkB,EAEnB,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,MAAM,MAAM,sBAAsB,GAAG;IACnC,4DAA4D;IAC5D,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,QAAQ,EAAE;QACR,gBAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QACtE,YAAY,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAClE,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAC5D,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAC7D,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAC9D,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;KAC9D,CAAC;IACF;;;;;;;;;;;OAWG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1D;;;;;;;;OAQG;IACH,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,EACpD,KAAK,EAAE,kBAAkB,EACzB,mBAAmB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,qBAAqB,CAAC;AACtD;;;;;;GAMG;AACH,QAAQ,CAAC,EAAE,MAAM,GAChB,sBAAsB,CA4VxB"}
|
|
@@ -26,6 +26,16 @@ export const LATE_RESUME_ADOPT_WINDOW_SEC = 3;
|
|
|
26
26
|
* is tracked in a component-scoped ref that outlives the element, so playback
|
|
27
27
|
* resumes from where it left off — never from `startAtSeconds` again and never
|
|
28
28
|
* from 0.
|
|
29
|
+
*
|
|
30
|
+
* The hook is also the single owner of the ACTIVE element generation: viewers
|
|
31
|
+
* attach their native element through `attachElement` (composed into the
|
|
32
|
+
* element ref) and call `retireElement` before performing a destructive media
|
|
33
|
+
* operation (`pause()` + src removal + `load()`, source replacement, DOM
|
|
34
|
+
* removal). A retiring element's still-live cursor is committed once as a
|
|
35
|
+
* final `pause` sample, and every media event a torn-down element emits
|
|
36
|
+
* afterwards (browsers reset the cursor to 0 and can fire pause/seeked/
|
|
37
|
+
* timeupdate during teardown) is ignored — so a teardown reset can never be
|
|
38
|
+
* presented to the app as a user playback event.
|
|
29
39
|
*/
|
|
30
40
|
export function useMediaPlayback(elementRef, props, playbackControlsRef,
|
|
31
41
|
/**
|
|
@@ -62,6 +72,26 @@ mediaKey) {
|
|
|
62
72
|
const seededRef = React.useRef(seedFromProps !== null);
|
|
63
73
|
// Throttle bookkeeping for periodic samples (wall-clock, active playback only).
|
|
64
74
|
const lastSampleAtRef = React.useRef(0);
|
|
75
|
+
// --- Active-element lifecycle (retirement boundary) -----------------------
|
|
76
|
+
// The element generation currently attached via `attachElement`, whether that
|
|
77
|
+
// generation is still ACTIVE (retirement flips this off), and whether this
|
|
78
|
+
// hook instance is lifecycle-managed at all (a consumer that never calls
|
|
79
|
+
// `attachElement` keeps the previous ungated behavior).
|
|
80
|
+
const attachedElRef = React.useRef(null);
|
|
81
|
+
const activeRef = React.useRef(false);
|
|
82
|
+
const managedRef = React.useRef(false);
|
|
83
|
+
// The position of the last commit-class (`pause`/`seeked`/`ended`) sample
|
|
84
|
+
// actually emitted. Retirement consults it so an already-reported final
|
|
85
|
+
// position (a pause/scrub/ended the app has just committed) is not emitted a
|
|
86
|
+
// second time by the retirement snapshot.
|
|
87
|
+
const lastCommittedRef = React.useRef(null);
|
|
88
|
+
// After a source swap on a RETAINED element, events queued by the outgoing
|
|
89
|
+
// source's teardown can still arrive addressed to the same element. Until the
|
|
90
|
+
// NEW source proves itself live (loadedmetadata, play, or a playing
|
|
91
|
+
// timeupdate), commit-class events are ignored — an old URL must not emit
|
|
92
|
+
// into the callback for a new URL. True from mount so a fresh element's
|
|
93
|
+
// pre-metadata pause/seek still reports as before.
|
|
94
|
+
const sourceSettledRef = React.useRef(true);
|
|
65
95
|
// Render-phase reset-on-source-change: when the media source (`mediaKey`)
|
|
66
96
|
// changes, the same component/hook instance is being reused for a DIFFERENT
|
|
67
97
|
// asset (e.g. a playlist swapping `url` in place). Reset every piece of
|
|
@@ -75,16 +105,70 @@ mediaKey) {
|
|
|
75
105
|
hasStartedRef.current = false;
|
|
76
106
|
seededRef.current = seedFromProps !== null;
|
|
77
107
|
lastSampleAtRef.current = 0;
|
|
108
|
+
lastCommittedRef.current = null;
|
|
109
|
+
sourceSettledRef.current = false;
|
|
78
110
|
if (hasEnded)
|
|
79
111
|
setHasEnded(false);
|
|
80
112
|
}
|
|
81
113
|
const emitSample = React.useCallback((el, reason) => {
|
|
114
|
+
if (reason !== "periodic") {
|
|
115
|
+
lastCommittedRef.current = el.currentTime;
|
|
116
|
+
}
|
|
82
117
|
const cb = onProgressSampleRef.current;
|
|
83
118
|
if (!cb)
|
|
84
119
|
return;
|
|
85
120
|
const duration = Number.isFinite(el.duration) ? el.duration : 0;
|
|
86
121
|
cb(el.currentTime, duration, reason);
|
|
87
122
|
}, []);
|
|
123
|
+
/**
|
|
124
|
+
* Is this event from the element generation that is allowed to report?
|
|
125
|
+
* Only enforced once the consumer opted into lifecycle management.
|
|
126
|
+
*/
|
|
127
|
+
const isFromActiveElement = React.useCallback((el) => {
|
|
128
|
+
if (!managedRef.current)
|
|
129
|
+
return true;
|
|
130
|
+
return activeRef.current && attachedElRef.current === el;
|
|
131
|
+
}, []);
|
|
132
|
+
// Retire the attached element: commit its still-live cursor once (final
|
|
133
|
+
// `pause` snapshot), then mark it inactive so later teardown-generated
|
|
134
|
+
// events are ignored. Snapshot rules: nothing to report unless playback
|
|
135
|
+
// actually started; skip when the last commit already reported this exact
|
|
136
|
+
// position; and honor the pre-seed suppression window — an autoplaying
|
|
137
|
+
// element torn down before the async resume decision settled must not write
|
|
138
|
+
// its near-0 drift over a saved position.
|
|
139
|
+
const doRetire = React.useCallback(() => {
|
|
140
|
+
const el = attachedElRef.current;
|
|
141
|
+
if (el && activeRef.current) {
|
|
142
|
+
if (hasStartedRef.current) {
|
|
143
|
+
const position = el.currentTime;
|
|
144
|
+
const preSeed = !seededRef.current && position <= LATE_RESUME_ADOPT_WINDOW_SEC;
|
|
145
|
+
if (!preSeed && lastCommittedRef.current !== position) {
|
|
146
|
+
lastPositionRef.current = position;
|
|
147
|
+
emitSample(el, "pause");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
activeRef.current = false;
|
|
152
|
+
}, [emitSample]);
|
|
153
|
+
const attachElement = React.useCallback((element) => {
|
|
154
|
+
managedRef.current = true;
|
|
155
|
+
if (element === attachedElRef.current && (element === null || activeRef.current)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (attachedElRef.current && attachedElRef.current !== element) {
|
|
159
|
+
doRetire();
|
|
160
|
+
}
|
|
161
|
+
attachedElRef.current = element;
|
|
162
|
+
activeRef.current = element !== null;
|
|
163
|
+
if (element !== null) {
|
|
164
|
+
// A newly attached element IS its current source — no stale teardown
|
|
165
|
+
// events can be queued against it yet.
|
|
166
|
+
sourceSettledRef.current = true;
|
|
167
|
+
}
|
|
168
|
+
}, [doRetire]);
|
|
169
|
+
const retireElement = React.useCallback(() => {
|
|
170
|
+
doRetire();
|
|
171
|
+
}, [doRetire]);
|
|
88
172
|
const trackPosition = React.useCallback((el) => {
|
|
89
173
|
if (hasStartedRef.current) {
|
|
90
174
|
lastPositionRef.current = el.currentTime;
|
|
@@ -143,14 +227,25 @@ mediaKey) {
|
|
|
143
227
|
applyInitialSeek(el, LATE_RESUME_ADOPT_WINDOW_SEC);
|
|
144
228
|
}, [seedFromProps, applyInitialSeek, elementRef]);
|
|
145
229
|
const onLoadedMetadata = React.useCallback((e) => {
|
|
146
|
-
|
|
147
|
-
|
|
230
|
+
const el = e.currentTarget;
|
|
231
|
+
if (!isFromActiveElement(el))
|
|
232
|
+
return;
|
|
233
|
+
// Metadata is the new source proving itself live after an in-place swap.
|
|
234
|
+
sourceSettledRef.current = true;
|
|
235
|
+
applyInitialSeek(el);
|
|
236
|
+
}, [applyInitialSeek, isFromActiveElement]);
|
|
148
237
|
const onTimeUpdate = React.useCallback((e) => {
|
|
149
238
|
const el = e.currentTarget;
|
|
239
|
+
// A retired element's teardown resets (currentTime → 0) must not corrupt
|
|
240
|
+
// the tracked resume position or produce samples.
|
|
241
|
+
if (!isFromActiveElement(el))
|
|
242
|
+
return;
|
|
150
243
|
// Any observed timeupdate while playing means playback has started; from
|
|
151
|
-
// now on we track live position for remount resume.
|
|
244
|
+
// now on we track live position for remount resume. A PLAYING timeupdate
|
|
245
|
+
// also proves the post-swap source live (teardown timeupdates are paused).
|
|
152
246
|
if (!el.paused && !el.ended) {
|
|
153
247
|
hasStartedRef.current = true;
|
|
248
|
+
sourceSettledRef.current = true;
|
|
154
249
|
}
|
|
155
250
|
trackPosition(el);
|
|
156
251
|
if (el.paused)
|
|
@@ -171,29 +266,43 @@ mediaKey) {
|
|
|
171
266
|
lastSampleAtRef.current = now;
|
|
172
267
|
emitSample(el, "periodic");
|
|
173
268
|
}
|
|
174
|
-
}, [emitSample, trackPosition]);
|
|
175
|
-
const onPlay = React.useCallback(() => {
|
|
269
|
+
}, [emitSample, trackPosition, isFromActiveElement]);
|
|
270
|
+
const onPlay = React.useCallback((e) => {
|
|
271
|
+
if (!isFromActiveElement(e.currentTarget))
|
|
272
|
+
return;
|
|
273
|
+
// A play event only ever comes from the current source (a torn-down
|
|
274
|
+
// source cannot start playing), so it settles a post-swap source too.
|
|
275
|
+
sourceSettledRef.current = true;
|
|
176
276
|
// Playing again after an ended state (native replay) clears the overlay.
|
|
177
277
|
setHasEnded(false);
|
|
178
|
-
}, []);
|
|
278
|
+
}, [isFromActiveElement]);
|
|
179
279
|
const onPause = React.useCallback((e) => {
|
|
180
280
|
const el = e.currentTarget;
|
|
281
|
+
// Retired-element and old-source teardown pauses are not user intent.
|
|
282
|
+
if (!isFromActiveElement(el) || !sourceSettledRef.current)
|
|
283
|
+
return;
|
|
181
284
|
trackPosition(el);
|
|
182
285
|
// Flush a sample immediately on pause (final position for this segment).
|
|
183
286
|
lastSampleAtRef.current = Date.now();
|
|
184
287
|
emitSample(el, "pause");
|
|
185
|
-
}, [emitSample, trackPosition]);
|
|
288
|
+
}, [emitSample, trackPosition, isFromActiveElement]);
|
|
186
289
|
const onSeeked = React.useCallback((e) => {
|
|
187
290
|
const el = e.currentTarget;
|
|
291
|
+
// A retired element's src-removal `load()` resets the cursor to 0 and can
|
|
292
|
+
// report it as a seek — that reset must never read as a user seek.
|
|
293
|
+
if (!isFromActiveElement(el) || !sourceSettledRef.current)
|
|
294
|
+
return;
|
|
188
295
|
trackPosition(el);
|
|
189
296
|
// Flush a sample on seek completion so the app sees the new position.
|
|
190
297
|
// This is also how the programmatic initial-resume seek reports the
|
|
191
298
|
// adopted position — the element fires `seeked` after applyInitialSeek.
|
|
192
299
|
lastSampleAtRef.current = Date.now();
|
|
193
300
|
emitSample(el, "seeked");
|
|
194
|
-
}, [emitSample, trackPosition]);
|
|
301
|
+
}, [emitSample, trackPosition, isFromActiveElement]);
|
|
195
302
|
const onEndedHandler = React.useCallback((e) => {
|
|
196
303
|
const el = e.currentTarget;
|
|
304
|
+
if (!isFromActiveElement(el) || !sourceSettledRef.current)
|
|
305
|
+
return;
|
|
197
306
|
// Ended means the play cursor reached duration; record final position.
|
|
198
307
|
hasStartedRef.current = true;
|
|
199
308
|
lastPositionRef.current = el.currentTime;
|
|
@@ -202,7 +311,7 @@ mediaKey) {
|
|
|
202
311
|
emitSample(el, "ended");
|
|
203
312
|
setHasEnded(true);
|
|
204
313
|
onEndedRef.current?.();
|
|
205
|
-
}, [emitSample]);
|
|
314
|
+
}, [emitSample, isFromActiveElement]);
|
|
206
315
|
// Imperative controls. restart() seeks to 0 and plays; seekTo(s) seeks.
|
|
207
316
|
React.useImperativeHandle(playbackControlsRef, () => ({
|
|
208
317
|
restart() {
|
|
@@ -248,6 +357,8 @@ mediaKey) {
|
|
|
248
357
|
onSeeked,
|
|
249
358
|
onEnded: onEndedHandler,
|
|
250
359
|
},
|
|
360
|
+
attachElement,
|
|
361
|
+
retireElement,
|
|
251
362
|
};
|
|
252
363
|
}
|
|
253
364
|
//# sourceMappingURL=use-media-playback.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-media-playback.js","sourceRoot":"","sources":["../../src/react/use-media-playback.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type {\n MediaPlaybackControls,\n MediaPlaybackProps,\n MediaProgressSampleReason,\n} from \"../types.js\";\n\n/**\n * Throttle interval for periodic progress samples. A sample fires at most once\n * per this many milliseconds of active (non-paused) playback via `timeupdate`.\n */\nexport const PROGRESS_SAMPLE_INTERVAL_MS = 5000;\n\n/**\n * How far an element may have played from the start and still ADOPT a\n * late-arriving `startAtSeconds`. Autoplay can begin playback before the app's\n * async resume position resolves (firefox reliably loses that race); a couple\n * of autoplayed seconds are not user intent, so the saved position still wins\n * inside this window. Beyond it, a late value is ignored — the user is\n * genuinely watching from where they are.\n */\nexport const LATE_RESUME_ADOPT_WINDOW_SEC = 3;\n\nexport type UseMediaPlaybackResult = {\n /** Whether the media has ended (drives the end overlay). */\n hasEnded: boolean;\n /** Handlers to spread onto the underlying <video>/<audio> element. */\n handlers: {\n onLoadedMetadata: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onTimeUpdate: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onPlay: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onPause: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onSeeked: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onEnded: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n };\n};\n\n/**\n * Shared playback API for VideoViewer and AudioViewer. Owns the additive\n * playback surface (onEnded, onProgressSample, startAtSeconds, endOverlay,\n * imperative controls) so both viewers behave identically. Adds NO observer —\n * everything is derived from native media element events (MEDIA-102).\n *\n * The element ref is supplied by the viewer (which already owns it for\n * autoplay/fullscreen). When the viewer unloads the element off-screen\n * (`unloadOnExit`) and remounts it on return, the last known playback position\n * is tracked in a component-scoped ref that outlives the element, so playback\n * resumes from where it left off — never from `startAtSeconds` again and never\n * from 0.\n */\nexport function useMediaPlayback(\n elementRef: React.RefObject<HTMLMediaElement | null>,\n props: MediaPlaybackProps,\n playbackControlsRef?: React.Ref<MediaPlaybackControls>,\n /**\n * Stable identity for the CURRENT media source (the viewer passes its `url`).\n * When this changes the media is a different asset, so all resume/progress/\n * ended tracking is reset — a stale position from the previous media must NOT\n * carry over onto the new element. Omit when the viewer never reuses one\n * instance across sources.\n */\n mediaKey?: string,\n): UseMediaPlaybackResult {\n const { onEnded, onProgressSample, startAtSeconds, endOverlay } = props;\n\n const [hasEnded, setHasEnded] = React.useState(false);\n\n // Latest callbacks/props via refs so handler identities stay stable and the\n // element event handlers always see current values.\n const onEndedRef = React.useRef(onEnded);\n const onProgressSampleRef = React.useRef(onProgressSample);\n onEndedRef.current = onEnded;\n onProgressSampleRef.current = onProgressSample;\n\n // The resume position implied by the CURRENT props: a positive\n // startAtSeconds, or null when the caller supplies none. One declaration —\n // the mount seed, the mediaKey reset, and the late-arrival effect all read it.\n const seedFromProps =\n typeof startAtSeconds === \"number\" && startAtSeconds > 0 ? startAtSeconds : null;\n\n // Last known playback position. Seeded from startAtSeconds, then updated once\n // playback has actually started. Survives the unloadOnExit unload/remount\n // cycle because it lives on the (still-mounted) component instance.\n const lastPositionRef = React.useRef<number | null>(seedFromProps);\n // True once we have observed real playback progress — after this we resume\n // from lastPositionRef, never re-apply the original startAtSeconds prop.\n const hasStartedRef = React.useRef(false);\n // True once a startAtSeconds seed has been ADOPTED (at mount or late-arrival).\n // Owns the adopt-exactly-once rule for the late-arrival effect below — playback\n // state is deliberately NOT that gate, because autoplay can start playback\n // before an async resume position arrives (see LATE_RESUME_ADOPT_WINDOW_SEC).\n const seededRef = React.useRef(seedFromProps !== null);\n\n // Throttle bookkeeping for periodic samples (wall-clock, active playback only).\n const lastSampleAtRef = React.useRef(0);\n\n // Render-phase reset-on-source-change: when the media source (`mediaKey`)\n // changes, the same component/hook instance is being reused for a DIFFERENT\n // asset (e.g. a playlist swapping `url` in place). Reset every piece of\n // per-source tracking so the previous media's position/ended/throttle state\n // cannot bleed onto the new element. This is the sanctioned render-phase\n // reset-on-prop-change pattern (not a set-state-in-effect).\n const prevMediaKeyRef = React.useRef<string | undefined>(mediaKey);\n if (prevMediaKeyRef.current !== mediaKey) {\n prevMediaKeyRef.current = mediaKey;\n lastPositionRef.current = seedFromProps;\n hasStartedRef.current = false;\n seededRef.current = seedFromProps !== null;\n lastSampleAtRef.current = 0;\n if (hasEnded) setHasEnded(false);\n }\n\n const emitSample = React.useCallback(\n (el: HTMLMediaElement, reason: MediaProgressSampleReason) => {\n const cb = onProgressSampleRef.current;\n if (!cb) return;\n const duration = Number.isFinite(el.duration) ? el.duration : 0;\n cb(el.currentTime, duration, reason);\n },\n [],\n );\n\n const trackPosition = React.useCallback((el: HTMLMediaElement) => {\n if (hasStartedRef.current) {\n lastPositionRef.current = el.currentTime;\n }\n }, []);\n\n const applyInitialSeek = React.useCallback((el: HTMLMediaElement, adoptWindowSec = 0.01) => {\n const target = lastPositionRef.current;\n if (typeof target !== \"number\" || target <= 0) return;\n // Only seek an element still near the start. The DEFAULT window keeps the\n // metadata handler idempotent (a metadata event on an already-positioned\n // element is a no-op) and re-applies the LAST KNOWN position to the new\n // element created by an unloadOnExit remount — resuming, never restarting.\n // The late-arrival effect widens the window (LATE_RESUME_ADOPT_WINDOW_SEC)\n // because autoplay may have advanced a little before the seed arrived.\n if (el.currentTime > adoptWindowSec) return;\n const dur = el.duration;\n const clamped = Number.isFinite(dur) && dur > 0 ? Math.min(target, dur) : target;\n try {\n el.currentTime = clamped;\n } catch {\n /* setting currentTime before ready can throw in some engines; ignore */\n }\n }, []);\n\n // Late-arriving resume position. `startAtSeconds` usually comes from an async\n // app source (a user-prefs query); when that resolves AFTER this hook mounted,\n // the mount-time seed above saw `undefined` and the element would silently\n // start at 0. Adopt the value when it finally arrives — exactly ONCE\n // (`seededRef`), and only while the element is within the small adoption\n // window of the start, so a late value can never yank media the user is\n // genuinely watching. Playback state is deliberately NOT the gate: under\n // AUTOPLAY the element starts playing from 0 on its own, and by the time the\n // prefs query resolves it has drifted a fraction of a second — firefox loses\n // that race on every deep-link resume (live 2026-07-31, hosted path 14.4:\n // expected ~224s, read 0.2s). A few autoplayed seconds are not user intent;\n // jumping to the user's saved position IS the requested behavior. Beyond the\n // window (a user genuinely re-watching from the top) the late value is\n // ignored, same as before. A `mediaKey` change resets the refs in the\n // render-phase block above, re-arming this for the new source; the seek is\n // applied here because the element may already have fired `loadedmetadata`\n // and will not fire it again — when metadata has not landed yet,\n // `onLoadedMetadata` applies it.\n React.useEffect(() => {\n if (seedFromProps === null) return;\n if (seededRef.current) return; // adopt exactly once; never override an applied seed\n const el = elementRef.current;\n if (el && el.currentTime > LATE_RESUME_ADOPT_WINDOW_SEC) return;\n seededRef.current = true;\n lastPositionRef.current = seedFromProps;\n if (el) applyInitialSeek(el, LATE_RESUME_ADOPT_WINDOW_SEC);\n }, [seedFromProps, applyInitialSeek, elementRef]);\n\n const onLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n applyInitialSeek(e.currentTarget);\n },\n [applyInitialSeek],\n );\n\n const onTimeUpdate = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n // Any observed timeupdate while playing means playback has started; from\n // now on we track live position for remount resume.\n if (!el.paused && !el.ended) {\n hasStartedRef.current = true;\n }\n trackPosition(el);\n if (el.paused) return; // no periodic samples while paused\n // Pre-seed suppression: until a resume position has been adopted, an\n // element still near the start may be autoplaying from 0 while the app's\n // async saved position is in flight. A periodic ~0 sample here would be\n // written to durable state and CLOBBER that saved position before the\n // seek lands. Stay silent for the adoption window; the seek's `seeked`\n // flush reports the resumed position, and with no saved position at all\n // periodic sampling simply begins a few seconds in (harmless). Deliberate\n // pause/seeked/ended flushes are never suppressed — a user pausing at\n // 0.5 s is genuine intent.\n if (!seededRef.current && el.currentTime <= LATE_RESUME_ADOPT_WINDOW_SEC) return;\n const now = Date.now();\n if (now - lastSampleAtRef.current >= PROGRESS_SAMPLE_INTERVAL_MS) {\n lastSampleAtRef.current = now;\n emitSample(el, \"periodic\");\n }\n },\n [emitSample, trackPosition],\n );\n\n const onPlay = React.useCallback(() => {\n // Playing again after an ended state (native replay) clears the overlay.\n setHasEnded(false);\n }, []);\n\n const onPause = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n trackPosition(el);\n // Flush a sample immediately on pause (final position for this segment).\n lastSampleAtRef.current = Date.now();\n emitSample(el, \"pause\");\n },\n [emitSample, trackPosition],\n );\n\n const onSeeked = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n trackPosition(el);\n // Flush a sample on seek completion so the app sees the new position.\n // This is also how the programmatic initial-resume seek reports the\n // adopted position — the element fires `seeked` after applyInitialSeek.\n lastSampleAtRef.current = Date.now();\n emitSample(el, \"seeked\");\n },\n [emitSample, trackPosition],\n );\n\n const onEndedHandler = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n // Ended means the play cursor reached duration; record final position.\n hasStartedRef.current = true;\n lastPositionRef.current = el.currentTime;\n // Flush a final sample with the ended position.\n lastSampleAtRef.current = Date.now();\n emitSample(el, \"ended\");\n setHasEnded(true);\n onEndedRef.current?.();\n },\n [emitSample],\n );\n\n // Imperative controls. restart() seeks to 0 and plays; seekTo(s) seeks.\n React.useImperativeHandle(\n playbackControlsRef,\n (): MediaPlaybackControls => ({\n restart() {\n const el = elementRef.current;\n if (!el) return;\n try {\n el.currentTime = 0;\n } catch {\n /* ignore */\n }\n lastPositionRef.current = 0;\n setHasEnded(false);\n void el.play().catch(() => {});\n },\n seekTo(seconds: number) {\n const el = elementRef.current;\n if (!el) return;\n const dur = el.duration;\n const clamped =\n Number.isFinite(dur) && dur > 0 ? Math.min(Math.max(seconds, 0), dur) : Math.max(seconds, 0);\n try {\n el.currentTime = clamped;\n } catch {\n /* ignore */\n }\n hasStartedRef.current = true;\n lastPositionRef.current = clamped;\n },\n }),\n [elementRef],\n );\n\n // Ended state is cleared by onPlay (native replay / imperative restart).\n // endOverlay itself is rendered by the viewer using `hasEnded`.\n void endOverlay;\n\n return {\n hasEnded,\n handlers: {\n onLoadedMetadata,\n onTimeUpdate,\n onPlay,\n onPause,\n onSeeked,\n onEnded: onEndedHandler,\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-media-playback.js","sourceRoot":"","sources":["../../src/react/use-media-playback.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type {\n MediaPlaybackControls,\n MediaPlaybackProps,\n MediaProgressSampleReason,\n} from \"../types.js\";\n\n/**\n * Throttle interval for periodic progress samples. A sample fires at most once\n * per this many milliseconds of active (non-paused) playback via `timeupdate`.\n */\nexport const PROGRESS_SAMPLE_INTERVAL_MS = 5000;\n\n/**\n * How far an element may have played from the start and still ADOPT a\n * late-arriving `startAtSeconds`. Autoplay can begin playback before the app's\n * async resume position resolves (firefox reliably loses that race); a couple\n * of autoplayed seconds are not user intent, so the saved position still wins\n * inside this window. Beyond it, a late value is ignored — the user is\n * genuinely watching from where they are.\n */\nexport const LATE_RESUME_ADOPT_WINDOW_SEC = 3;\n\nexport type UseMediaPlaybackResult = {\n /** Whether the media has ended (drives the end overlay). */\n hasEnded: boolean;\n /** Handlers to spread onto the underlying <video>/<audio> element. */\n handlers: {\n onLoadedMetadata: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onTimeUpdate: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onPlay: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onPause: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onSeeked: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n onEnded: (e: React.SyntheticEvent<HTMLMediaElement>) => void;\n };\n /**\n * Element lifecycle boundary. Attach the native element through this callback\n * (compose it into the element `ref`) so the hook knows which element\n * generation is ACTIVE. A detach (`null`) retires the outgoing element:\n * its still-live cursor is committed as one final `pause` sample (unless the\n * last commit already reported that exact position, playback never started,\n * or the position is still inside the pre-seed suppression window), and every\n * later `pause`/`seeked`/`ended`/`timeupdate` event from that retired element\n * is ignored — a teardown-generated reset can never masquerade as a user\n * playback event. Opt-in: a consumer that never calls this keeps the previous\n * ungated behavior.\n */\n attachElement: (element: HTMLMediaElement | null) => void;\n /**\n * Retire the currently attached element NOW, before a destructive media\n * operation the caller is about to perform on it (`pause()` +\n * `removeAttribute(\"src\")` + `load()`, DOM removal, source replacement).\n * Emits the same final-snapshot commit as a detach and stops all further\n * reporting from that element. Idempotent — a later detach of an already\n * retired element emits nothing. Re-attaching an element through\n * `attachElement` reactivates reporting.\n */\n retireElement: () => void;\n};\n\n/**\n * Shared playback API for VideoViewer and AudioViewer. Owns the additive\n * playback surface (onEnded, onProgressSample, startAtSeconds, endOverlay,\n * imperative controls) so both viewers behave identically. Adds NO observer —\n * everything is derived from native media element events (MEDIA-102).\n *\n * The element ref is supplied by the viewer (which already owns it for\n * autoplay/fullscreen). When the viewer unloads the element off-screen\n * (`unloadOnExit`) and remounts it on return, the last known playback position\n * is tracked in a component-scoped ref that outlives the element, so playback\n * resumes from where it left off — never from `startAtSeconds` again and never\n * from 0.\n *\n * The hook is also the single owner of the ACTIVE element generation: viewers\n * attach their native element through `attachElement` (composed into the\n * element ref) and call `retireElement` before performing a destructive media\n * operation (`pause()` + src removal + `load()`, source replacement, DOM\n * removal). A retiring element's still-live cursor is committed once as a\n * final `pause` sample, and every media event a torn-down element emits\n * afterwards (browsers reset the cursor to 0 and can fire pause/seeked/\n * timeupdate during teardown) is ignored — so a teardown reset can never be\n * presented to the app as a user playback event.\n */\nexport function useMediaPlayback(\n elementRef: React.RefObject<HTMLMediaElement | null>,\n props: MediaPlaybackProps,\n playbackControlsRef?: React.Ref<MediaPlaybackControls>,\n /**\n * Stable identity for the CURRENT media source (the viewer passes its `url`).\n * When this changes the media is a different asset, so all resume/progress/\n * ended tracking is reset — a stale position from the previous media must NOT\n * carry over onto the new element. Omit when the viewer never reuses one\n * instance across sources.\n */\n mediaKey?: string,\n): UseMediaPlaybackResult {\n const { onEnded, onProgressSample, startAtSeconds, endOverlay } = props;\n\n const [hasEnded, setHasEnded] = React.useState(false);\n\n // Latest callbacks/props via refs so handler identities stay stable and the\n // element event handlers always see current values.\n const onEndedRef = React.useRef(onEnded);\n const onProgressSampleRef = React.useRef(onProgressSample);\n onEndedRef.current = onEnded;\n onProgressSampleRef.current = onProgressSample;\n\n // The resume position implied by the CURRENT props: a positive\n // startAtSeconds, or null when the caller supplies none. One declaration —\n // the mount seed, the mediaKey reset, and the late-arrival effect all read it.\n const seedFromProps =\n typeof startAtSeconds === \"number\" && startAtSeconds > 0 ? startAtSeconds : null;\n\n // Last known playback position. Seeded from startAtSeconds, then updated once\n // playback has actually started. Survives the unloadOnExit unload/remount\n // cycle because it lives on the (still-mounted) component instance.\n const lastPositionRef = React.useRef<number | null>(seedFromProps);\n // True once we have observed real playback progress — after this we resume\n // from lastPositionRef, never re-apply the original startAtSeconds prop.\n const hasStartedRef = React.useRef(false);\n // True once a startAtSeconds seed has been ADOPTED (at mount or late-arrival).\n // Owns the adopt-exactly-once rule for the late-arrival effect below — playback\n // state is deliberately NOT that gate, because autoplay can start playback\n // before an async resume position arrives (see LATE_RESUME_ADOPT_WINDOW_SEC).\n const seededRef = React.useRef(seedFromProps !== null);\n\n // Throttle bookkeeping for periodic samples (wall-clock, active playback only).\n const lastSampleAtRef = React.useRef(0);\n\n // --- Active-element lifecycle (retirement boundary) -----------------------\n // The element generation currently attached via `attachElement`, whether that\n // generation is still ACTIVE (retirement flips this off), and whether this\n // hook instance is lifecycle-managed at all (a consumer that never calls\n // `attachElement` keeps the previous ungated behavior).\n const attachedElRef = React.useRef<HTMLMediaElement | null>(null);\n const activeRef = React.useRef(false);\n const managedRef = React.useRef(false);\n // The position of the last commit-class (`pause`/`seeked`/`ended`) sample\n // actually emitted. Retirement consults it so an already-reported final\n // position (a pause/scrub/ended the app has just committed) is not emitted a\n // second time by the retirement snapshot.\n const lastCommittedRef = React.useRef<number | null>(null);\n // After a source swap on a RETAINED element, events queued by the outgoing\n // source's teardown can still arrive addressed to the same element. Until the\n // NEW source proves itself live (loadedmetadata, play, or a playing\n // timeupdate), commit-class events are ignored — an old URL must not emit\n // into the callback for a new URL. True from mount so a fresh element's\n // pre-metadata pause/seek still reports as before.\n const sourceSettledRef = React.useRef(true);\n\n // Render-phase reset-on-source-change: when the media source (`mediaKey`)\n // changes, the same component/hook instance is being reused for a DIFFERENT\n // asset (e.g. a playlist swapping `url` in place). Reset every piece of\n // per-source tracking so the previous media's position/ended/throttle state\n // cannot bleed onto the new element. This is the sanctioned render-phase\n // reset-on-prop-change pattern (not a set-state-in-effect).\n const prevMediaKeyRef = React.useRef<string | undefined>(mediaKey);\n if (prevMediaKeyRef.current !== mediaKey) {\n prevMediaKeyRef.current = mediaKey;\n lastPositionRef.current = seedFromProps;\n hasStartedRef.current = false;\n seededRef.current = seedFromProps !== null;\n lastSampleAtRef.current = 0;\n lastCommittedRef.current = null;\n sourceSettledRef.current = false;\n if (hasEnded) setHasEnded(false);\n }\n\n const emitSample = React.useCallback(\n (el: HTMLMediaElement, reason: MediaProgressSampleReason) => {\n if (reason !== \"periodic\") {\n lastCommittedRef.current = el.currentTime;\n }\n const cb = onProgressSampleRef.current;\n if (!cb) return;\n const duration = Number.isFinite(el.duration) ? el.duration : 0;\n cb(el.currentTime, duration, reason);\n },\n [],\n );\n\n /**\n * Is this event from the element generation that is allowed to report?\n * Only enforced once the consumer opted into lifecycle management.\n */\n const isFromActiveElement = React.useCallback((el: HTMLMediaElement) => {\n if (!managedRef.current) return true;\n return activeRef.current && attachedElRef.current === el;\n }, []);\n\n // Retire the attached element: commit its still-live cursor once (final\n // `pause` snapshot), then mark it inactive so later teardown-generated\n // events are ignored. Snapshot rules: nothing to report unless playback\n // actually started; skip when the last commit already reported this exact\n // position; and honor the pre-seed suppression window — an autoplaying\n // element torn down before the async resume decision settled must not write\n // its near-0 drift over a saved position.\n const doRetire = React.useCallback(() => {\n const el = attachedElRef.current;\n if (el && activeRef.current) {\n if (hasStartedRef.current) {\n const position = el.currentTime;\n const preSeed =\n !seededRef.current && position <= LATE_RESUME_ADOPT_WINDOW_SEC;\n if (!preSeed && lastCommittedRef.current !== position) {\n lastPositionRef.current = position;\n emitSample(el, \"pause\");\n }\n }\n }\n activeRef.current = false;\n }, [emitSample]);\n\n const attachElement = React.useCallback(\n (element: HTMLMediaElement | null) => {\n managedRef.current = true;\n if (element === attachedElRef.current && (element === null || activeRef.current)) {\n return;\n }\n if (attachedElRef.current && attachedElRef.current !== element) {\n doRetire();\n }\n attachedElRef.current = element;\n activeRef.current = element !== null;\n if (element !== null) {\n // A newly attached element IS its current source — no stale teardown\n // events can be queued against it yet.\n sourceSettledRef.current = true;\n }\n },\n [doRetire],\n );\n\n const retireElement = React.useCallback(() => {\n doRetire();\n }, [doRetire]);\n\n const trackPosition = React.useCallback((el: HTMLMediaElement) => {\n if (hasStartedRef.current) {\n lastPositionRef.current = el.currentTime;\n }\n }, []);\n\n const applyInitialSeek = React.useCallback((el: HTMLMediaElement, adoptWindowSec = 0.01) => {\n const target = lastPositionRef.current;\n if (typeof target !== \"number\" || target <= 0) return;\n // Only seek an element still near the start. The DEFAULT window keeps the\n // metadata handler idempotent (a metadata event on an already-positioned\n // element is a no-op) and re-applies the LAST KNOWN position to the new\n // element created by an unloadOnExit remount — resuming, never restarting.\n // The late-arrival effect widens the window (LATE_RESUME_ADOPT_WINDOW_SEC)\n // because autoplay may have advanced a little before the seed arrived.\n if (el.currentTime > adoptWindowSec) return;\n const dur = el.duration;\n const clamped = Number.isFinite(dur) && dur > 0 ? Math.min(target, dur) : target;\n try {\n el.currentTime = clamped;\n } catch {\n /* setting currentTime before ready can throw in some engines; ignore */\n }\n }, []);\n\n // Late-arriving resume position. `startAtSeconds` usually comes from an async\n // app source (a user-prefs query); when that resolves AFTER this hook mounted,\n // the mount-time seed above saw `undefined` and the element would silently\n // start at 0. Adopt the value when it finally arrives — exactly ONCE\n // (`seededRef`), and only while the element is within the small adoption\n // window of the start, so a late value can never yank media the user is\n // genuinely watching. Playback state is deliberately NOT the gate: under\n // AUTOPLAY the element starts playing from 0 on its own, and by the time the\n // prefs query resolves it has drifted a fraction of a second — firefox loses\n // that race on every deep-link resume (live 2026-07-31, hosted path 14.4:\n // expected ~224s, read 0.2s). A few autoplayed seconds are not user intent;\n // jumping to the user's saved position IS the requested behavior. Beyond the\n // window (a user genuinely re-watching from the top) the late value is\n // ignored, same as before. A `mediaKey` change resets the refs in the\n // render-phase block above, re-arming this for the new source; the seek is\n // applied here because the element may already have fired `loadedmetadata`\n // and will not fire it again — when metadata has not landed yet,\n // `onLoadedMetadata` applies it.\n React.useEffect(() => {\n if (seedFromProps === null) return;\n if (seededRef.current) return; // adopt exactly once; never override an applied seed\n const el = elementRef.current;\n if (el && el.currentTime > LATE_RESUME_ADOPT_WINDOW_SEC) return;\n seededRef.current = true;\n lastPositionRef.current = seedFromProps;\n if (el) applyInitialSeek(el, LATE_RESUME_ADOPT_WINDOW_SEC);\n }, [seedFromProps, applyInitialSeek, elementRef]);\n\n const onLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n if (!isFromActiveElement(el)) return;\n // Metadata is the new source proving itself live after an in-place swap.\n sourceSettledRef.current = true;\n applyInitialSeek(el);\n },\n [applyInitialSeek, isFromActiveElement],\n );\n\n const onTimeUpdate = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n // A retired element's teardown resets (currentTime → 0) must not corrupt\n // the tracked resume position or produce samples.\n if (!isFromActiveElement(el)) return;\n // Any observed timeupdate while playing means playback has started; from\n // now on we track live position for remount resume. A PLAYING timeupdate\n // also proves the post-swap source live (teardown timeupdates are paused).\n if (!el.paused && !el.ended) {\n hasStartedRef.current = true;\n sourceSettledRef.current = true;\n }\n trackPosition(el);\n if (el.paused) return; // no periodic samples while paused\n // Pre-seed suppression: until a resume position has been adopted, an\n // element still near the start may be autoplaying from 0 while the app's\n // async saved position is in flight. A periodic ~0 sample here would be\n // written to durable state and CLOBBER that saved position before the\n // seek lands. Stay silent for the adoption window; the seek's `seeked`\n // flush reports the resumed position, and with no saved position at all\n // periodic sampling simply begins a few seconds in (harmless). Deliberate\n // pause/seeked/ended flushes are never suppressed — a user pausing at\n // 0.5 s is genuine intent.\n if (!seededRef.current && el.currentTime <= LATE_RESUME_ADOPT_WINDOW_SEC) return;\n const now = Date.now();\n if (now - lastSampleAtRef.current >= PROGRESS_SAMPLE_INTERVAL_MS) {\n lastSampleAtRef.current = now;\n emitSample(el, \"periodic\");\n }\n },\n [emitSample, trackPosition, isFromActiveElement],\n );\n\n const onPlay = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n if (!isFromActiveElement(e.currentTarget)) return;\n // A play event only ever comes from the current source (a torn-down\n // source cannot start playing), so it settles a post-swap source too.\n sourceSettledRef.current = true;\n // Playing again after an ended state (native replay) clears the overlay.\n setHasEnded(false);\n },\n [isFromActiveElement],\n );\n\n const onPause = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n // Retired-element and old-source teardown pauses are not user intent.\n if (!isFromActiveElement(el) || !sourceSettledRef.current) return;\n trackPosition(el);\n // Flush a sample immediately on pause (final position for this segment).\n lastSampleAtRef.current = Date.now();\n emitSample(el, \"pause\");\n },\n [emitSample, trackPosition, isFromActiveElement],\n );\n\n const onSeeked = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n // A retired element's src-removal `load()` resets the cursor to 0 and can\n // report it as a seek — that reset must never read as a user seek.\n if (!isFromActiveElement(el) || !sourceSettledRef.current) return;\n trackPosition(el);\n // Flush a sample on seek completion so the app sees the new position.\n // This is also how the programmatic initial-resume seek reports the\n // adopted position — the element fires `seeked` after applyInitialSeek.\n lastSampleAtRef.current = Date.now();\n emitSample(el, \"seeked\");\n },\n [emitSample, trackPosition, isFromActiveElement],\n );\n\n const onEndedHandler = React.useCallback(\n (e: React.SyntheticEvent<HTMLMediaElement>) => {\n const el = e.currentTarget;\n if (!isFromActiveElement(el) || !sourceSettledRef.current) return;\n // Ended means the play cursor reached duration; record final position.\n hasStartedRef.current = true;\n lastPositionRef.current = el.currentTime;\n // Flush a final sample with the ended position.\n lastSampleAtRef.current = Date.now();\n emitSample(el, \"ended\");\n setHasEnded(true);\n onEndedRef.current?.();\n },\n [emitSample, isFromActiveElement],\n );\n\n // Imperative controls. restart() seeks to 0 and plays; seekTo(s) seeks.\n React.useImperativeHandle(\n playbackControlsRef,\n (): MediaPlaybackControls => ({\n restart() {\n const el = elementRef.current;\n if (!el) return;\n try {\n el.currentTime = 0;\n } catch {\n /* ignore */\n }\n lastPositionRef.current = 0;\n setHasEnded(false);\n void el.play().catch(() => {});\n },\n seekTo(seconds: number) {\n const el = elementRef.current;\n if (!el) return;\n const dur = el.duration;\n const clamped =\n Number.isFinite(dur) && dur > 0 ? Math.min(Math.max(seconds, 0), dur) : Math.max(seconds, 0);\n try {\n el.currentTime = clamped;\n } catch {\n /* ignore */\n }\n hasStartedRef.current = true;\n lastPositionRef.current = clamped;\n },\n }),\n [elementRef],\n );\n\n // Ended state is cleared by onPlay (native replay / imperative restart).\n // endOverlay itself is rendered by the viewer using `hasEnded`.\n void endOverlay;\n\n return {\n hasEnded,\n handlers: {\n onLoadedMetadata,\n onTimeUpdate,\n onPlay,\n onPause,\n onSeeked,\n onEnded: onEndedHandler,\n },\n attachElement,\n retireElement,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-viewer.d.ts","sourceRoot":"","sources":["../../src/react/video-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"video-viewer.d.ts","sourceRoot":"","sources":["../../src/react/video-viewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBAwOlD"}
|
|
@@ -16,7 +16,13 @@ export function VideoViewer(props) {
|
|
|
16
16
|
// Additive playback API (onEnded, progress sampling, startAt/resume,
|
|
17
17
|
// endOverlay, imperative controls). Derives everything from element events —
|
|
18
18
|
// adds no observer (MEDIA-102).
|
|
19
|
-
const { hasEnded, handlers: playbackHandlers } = useMediaPlayback(videoRef, { onEnded, onProgressSample, startAtSeconds, endOverlay }, playbackControlsRef, url);
|
|
19
|
+
const { hasEnded, handlers: playbackHandlers, attachElement, retireElement } = useMediaPlayback(videoRef, { onEnded, onProgressSample, startAtSeconds, endOverlay }, playbackControlsRef, url);
|
|
20
|
+
// The element attaches through the playback lifecycle so the hook knows the
|
|
21
|
+
// active generation; detach (unmount) retires it.
|
|
22
|
+
const setVideoRef = React.useCallback((el) => {
|
|
23
|
+
videoRef.current = el;
|
|
24
|
+
attachElement(el);
|
|
25
|
+
}, [attachElement]);
|
|
20
26
|
// Single observer for both lazy-load gating AND autoplay-on-visible.
|
|
21
27
|
// threshold: [0, 0.5] — 0 fires "any pixel visible" (drives shouldLoad),
|
|
22
28
|
// 0.5 fires "at least half visible" (drives autoplay/pause).
|
|
@@ -34,6 +40,9 @@ export function VideoViewer(props) {
|
|
|
34
40
|
if (!inView && unloadOnExit && shouldLoad && !priority && lazy) {
|
|
35
41
|
const video = videoRef.current;
|
|
36
42
|
if (video) {
|
|
43
|
+
// Retire FIRST: snapshot/commit the still-live cursor, then ignore the
|
|
44
|
+
// pause/seek/reset events the teardown below makes the element emit.
|
|
45
|
+
retireElement();
|
|
37
46
|
video.pause();
|
|
38
47
|
video.removeAttribute("src");
|
|
39
48
|
video.load();
|
|
@@ -42,7 +51,7 @@ export function VideoViewer(props) {
|
|
|
42
51
|
setHasMetadata(false);
|
|
43
52
|
setShouldLoad(false);
|
|
44
53
|
}
|
|
45
|
-
}, [inView, shouldLoad, unloadOnExit, priority, lazy]);
|
|
54
|
+
}, [inView, shouldLoad, unloadOnExit, priority, lazy, retireElement]);
|
|
46
55
|
React.useEffect(() => {
|
|
47
56
|
setHasError(false);
|
|
48
57
|
setIsLoaded(false);
|
|
@@ -110,7 +119,7 @@ export function VideoViewer(props) {
|
|
|
110
119
|
return _jsx(_Fragment, { children: fallback });
|
|
111
120
|
return (_jsx("div", { className: className, children: _jsx("div", { className: "mv-video-error", children: _jsx("p", { children: "Failed to load video" }) }) }));
|
|
112
121
|
}
|
|
113
|
-
return (_jsxs("div", { ref: inViewRef, className: className, role: enableFullscreen ? "button" : undefined, tabIndex: enableFullscreen ? 0 : undefined, "aria-label": enableFullscreen ? "Open video fullscreen" : undefined, onDoubleClick: requestFullscreen, onKeyDown: onKeyDown, style: { position: "relative", width: "100%", height: "100%", overflow: "hidden", outline: "none" }, children: [showSkeleton && !isLoaded && shouldLoad && (_jsx(Skeleton, { style: { position: "absolute", inset: 0, width: "100%", height: "100%" } })), shouldLoad && (_jsx("video", { ref:
|
|
122
|
+
return (_jsxs("div", { ref: inViewRef, className: className, role: enableFullscreen ? "button" : undefined, tabIndex: enableFullscreen ? 0 : undefined, "aria-label": enableFullscreen ? "Open video fullscreen" : undefined, onDoubleClick: requestFullscreen, onKeyDown: onKeyDown, style: { position: "relative", width: "100%", height: "100%", overflow: "hidden", outline: "none" }, children: [showSkeleton && !isLoaded && shouldLoad && (_jsx(Skeleton, { style: { position: "absolute", inset: 0, width: "100%", height: "100%" } })), shouldLoad && (_jsx("video", { ref: setVideoRef, src: url, className: mediaClassName, controls: controls, autoPlay: autoPlay, muted: resolvedMuted, loop: loop, poster: posterUrl, onLoadedData: handleLoadedData, onError: handleError, onLoadedMetadata: handleLoadedMetadata, onTimeUpdate: playbackHandlers.onTimeUpdate, onPlay: playbackHandlers.onPlay, onPause: playbackHandlers.onPause, onSeeked: playbackHandlers.onSeeked, onEnded: playbackHandlers.onEnded, preload: priority ? "auto" : preload, playsInline: true, style: {
|
|
114
123
|
width: "100%",
|
|
115
124
|
height: "100%",
|
|
116
125
|
objectFit: "cover",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-viewer.js","sourceRoot":"","sources":["../../src/react/video-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { VideoViewerProps } from \"../types.js\";\nimport { useMediaPlayback } from \"./use-media-playback.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function VideoViewer(props: VideoViewerProps) {\n const {\n url,\n className,\n mediaClassName,\n controls = true,\n autoPlay = false,\n muted,\n loop = false,\n posterUrl,\n enableFullscreen = true,\n lazy = true,\n unloadOnExit = true,\n priority = false,\n skeleton: showSkeleton = true,\n preload = \"metadata\",\n autoPlayOnVisible = false,\n onLoad,\n onError,\n fallback,\n loadTimeoutMs,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n } = props;\n\n const resolvedMuted = muted ?? autoPlay;\n const autoplayActive = autoPlayOnVisible || autoPlay;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [hasMetadata, setHasMetadata] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const videoRef = React.useRef<HTMLVideoElement>(null);\n\n // Additive playback API (onEnded, progress sampling, startAt/resume,\n // endOverlay, imperative controls). Derives everything from element events —\n // adds no observer (MEDIA-102).\n const { hasEnded, handlers: playbackHandlers } = useMediaPlayback(\n videoRef,\n { onEnded, onProgressSample, startAtSeconds, endOverlay },\n playbackControlsRef,\n url,\n );\n\n // Single observer for both lazy-load gating AND autoplay-on-visible.\n // threshold: [0, 0.5] — 0 fires \"any pixel visible\" (drives shouldLoad),\n // 0.5 fires \"at least half visible\" (drives autoplay/pause).\n const { ref: inViewRef, inView, entry } = useInView({\n triggerOnce: !unloadOnExit && !autoplayActive,\n threshold: [0, 0.5],\n rootMargin: \"200px\",\n skip: !autoplayActive && (priority || !lazy),\n });\n\n const isFullyVisible = (entry?.intersectionRatio ?? 0) >= 0.5;\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n if (!inView && unloadOnExit && shouldLoad && !priority && lazy) {\n const video = videoRef.current;\n if (video) {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n }\n setIsLoaded(false);\n setHasMetadata(false);\n setShouldLoad(false);\n }\n }, [inView, shouldLoad, unloadOnExit, priority, lazy]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n setHasMetadata(false);\n }, [url]);\n\n const handleLoadedData = React.useCallback(() => {\n setIsLoaded(true);\n onLoad?.();\n }, [onLoad]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n }, [onError]);\n\n // Watchdog settles on METADATA, not loadeddata — a healthy preload=\"metadata\"\n // list video may not fire loadeddata until play, and must not false-positive.\n const handleLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLVideoElement>) => {\n setHasMetadata(true);\n playbackHandlers.onLoadedMetadata(e);\n },\n [playbackHandlers]\n );\n\n useLoadWatchdog(\n shouldLoad && !isLoaded && !hasMetadata && !hasError,\n loadTimeoutMs,\n handleError\n );\n\n // Autoplay on >=50% visible / pause when below (same single observer above)\n React.useEffect(() => {\n if (!autoplayActive) return;\n const video = videoRef.current;\n if (!video || !shouldLoad) return;\n if (isFullyVisible) {\n if (video.paused) {\n video.play().catch(() => {});\n }\n } else if (!video.paused) {\n video.pause();\n }\n }, [autoplayActive, shouldLoad, isFullyVisible]);\n\n // Fullscreen\n const requestFullscreen = React.useCallback(() => {\n if (!enableFullscreen) return;\n const el = videoRef.current as HTMLVideoElement & {\n webkitRequestFullscreen?: () => void;\n mozRequestFullScreen?: () => void;\n msRequestFullscreen?: () => void;\n };\n if (!el) return;\n const fn =\n el.requestFullscreen ||\n el.webkitRequestFullscreen ||\n el.mozRequestFullScreen ||\n el.msRequestFullscreen;\n if (typeof fn === \"function\") {\n try { fn.call(el); } catch { /* ignore */ }\n }\n }, [enableFullscreen]);\n\n const onKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n if (!enableFullscreen) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n requestFullscreen();\n }\n },\n [enableFullscreen, requestFullscreen]\n );\n\n if (hasError) {\n if (fallback) return <>{fallback}</>;\n return (\n <div className={className}>\n <div className=\"mv-video-error\">\n <p>Failed to load video</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n ref={inViewRef}\n className={className}\n role={enableFullscreen ? \"button\" : undefined}\n tabIndex={enableFullscreen ? 0 : undefined}\n aria-label={enableFullscreen ? \"Open video fullscreen\" : undefined}\n onDoubleClick={requestFullscreen}\n onKeyDown={onKeyDown}\n style={{ position: \"relative\", width: \"100%\", height: \"100%\", overflow: \"hidden\", outline: \"none\" }}\n >\n {showSkeleton && !isLoaded && shouldLoad && (\n <Skeleton style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }} />\n )}\n {shouldLoad && (\n <video\n ref={videoRef}\n src={url}\n className={mediaClassName}\n controls={controls}\n autoPlay={autoPlay}\n muted={resolvedMuted}\n loop={loop}\n poster={posterUrl}\n onLoadedData={handleLoadedData}\n onError={handleError}\n onLoadedMetadata={handleLoadedMetadata}\n onTimeUpdate={playbackHandlers.onTimeUpdate}\n onPlay={playbackHandlers.onPlay}\n onPause={playbackHandlers.onPause}\n onSeeked={playbackHandlers.onSeeked}\n onEnded={playbackHandlers.onEnded}\n preload={priority ? \"auto\" : preload}\n playsInline\n style={{\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity 300ms ease\",\n WebkitUserSelect: \"none\",\n WebkitTouchCallout: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n } as React.CSSProperties}\n />\n )}\n {endOverlay != null && hasEnded && (\n <div\n className=\"mv-end-overlay\"\n style={{ position: \"absolute\", inset: 0, zIndex: 2 }}\n >\n {endOverlay}\n </div>\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"video-viewer.js","sourceRoot":"","sources":["../../src/react/video-viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import * as React from \"react\";\nimport { useInView } from \"react-intersection-observer\";\nimport { Skeleton } from \"@ttt-productions/ui-core/react\";\nimport type { VideoViewerProps } from \"../types.js\";\nimport { useMediaPlayback } from \"./use-media-playback.js\";\nimport { useLoadWatchdog } from \"./use-load-watchdog.js\";\n\nexport function VideoViewer(props: VideoViewerProps) {\n const {\n url,\n className,\n mediaClassName,\n controls = true,\n autoPlay = false,\n muted,\n loop = false,\n posterUrl,\n enableFullscreen = true,\n lazy = true,\n unloadOnExit = true,\n priority = false,\n skeleton: showSkeleton = true,\n preload = \"metadata\",\n autoPlayOnVisible = false,\n onLoad,\n onError,\n fallback,\n loadTimeoutMs,\n onEnded,\n onProgressSample,\n startAtSeconds,\n endOverlay,\n playbackControlsRef,\n } = props;\n\n const resolvedMuted = muted ?? autoPlay;\n const autoplayActive = autoPlayOnVisible || autoPlay;\n\n const [isLoaded, setIsLoaded] = React.useState(false);\n const [hasError, setHasError] = React.useState(false);\n const [hasMetadata, setHasMetadata] = React.useState(false);\n const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);\n const videoRef = React.useRef<HTMLVideoElement>(null);\n\n // Additive playback API (onEnded, progress sampling, startAt/resume,\n // endOverlay, imperative controls). Derives everything from element events —\n // adds no observer (MEDIA-102).\n const { hasEnded, handlers: playbackHandlers, attachElement, retireElement } = useMediaPlayback(\n videoRef,\n { onEnded, onProgressSample, startAtSeconds, endOverlay },\n playbackControlsRef,\n url,\n );\n\n // The element attaches through the playback lifecycle so the hook knows the\n // active generation; detach (unmount) retires it.\n const setVideoRef = React.useCallback(\n (el: HTMLVideoElement | null) => {\n videoRef.current = el;\n attachElement(el);\n },\n [attachElement],\n );\n\n // Single observer for both lazy-load gating AND autoplay-on-visible.\n // threshold: [0, 0.5] — 0 fires \"any pixel visible\" (drives shouldLoad),\n // 0.5 fires \"at least half visible\" (drives autoplay/pause).\n const { ref: inViewRef, inView, entry } = useInView({\n triggerOnce: !unloadOnExit && !autoplayActive,\n threshold: [0, 0.5],\n rootMargin: \"200px\",\n skip: !autoplayActive && (priority || !lazy),\n });\n\n const isFullyVisible = (entry?.intersectionRatio ?? 0) >= 0.5;\n\n React.useEffect(() => {\n if (inView && !shouldLoad) {\n setShouldLoad(true);\n }\n if (!inView && unloadOnExit && shouldLoad && !priority && lazy) {\n const video = videoRef.current;\n if (video) {\n // Retire FIRST: snapshot/commit the still-live cursor, then ignore the\n // pause/seek/reset events the teardown below makes the element emit.\n retireElement();\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n }\n setIsLoaded(false);\n setHasMetadata(false);\n setShouldLoad(false);\n }\n }, [inView, shouldLoad, unloadOnExit, priority, lazy, retireElement]);\n\n React.useEffect(() => {\n setHasError(false);\n setIsLoaded(false);\n setHasMetadata(false);\n }, [url]);\n\n const handleLoadedData = React.useCallback(() => {\n setIsLoaded(true);\n onLoad?.();\n }, [onLoad]);\n\n const handleError = React.useCallback(() => {\n setHasError(true);\n onError?.();\n }, [onError]);\n\n // Watchdog settles on METADATA, not loadeddata — a healthy preload=\"metadata\"\n // list video may not fire loadeddata until play, and must not false-positive.\n const handleLoadedMetadata = React.useCallback(\n (e: React.SyntheticEvent<HTMLVideoElement>) => {\n setHasMetadata(true);\n playbackHandlers.onLoadedMetadata(e);\n },\n [playbackHandlers]\n );\n\n useLoadWatchdog(\n shouldLoad && !isLoaded && !hasMetadata && !hasError,\n loadTimeoutMs,\n handleError\n );\n\n // Autoplay on >=50% visible / pause when below (same single observer above)\n React.useEffect(() => {\n if (!autoplayActive) return;\n const video = videoRef.current;\n if (!video || !shouldLoad) return;\n if (isFullyVisible) {\n if (video.paused) {\n video.play().catch(() => {});\n }\n } else if (!video.paused) {\n video.pause();\n }\n }, [autoplayActive, shouldLoad, isFullyVisible]);\n\n // Fullscreen\n const requestFullscreen = React.useCallback(() => {\n if (!enableFullscreen) return;\n const el = videoRef.current as HTMLVideoElement & {\n webkitRequestFullscreen?: () => void;\n mozRequestFullScreen?: () => void;\n msRequestFullscreen?: () => void;\n };\n if (!el) return;\n const fn =\n el.requestFullscreen ||\n el.webkitRequestFullscreen ||\n el.mozRequestFullScreen ||\n el.msRequestFullscreen;\n if (typeof fn === \"function\") {\n try { fn.call(el); } catch { /* ignore */ }\n }\n }, [enableFullscreen]);\n\n const onKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n if (!enableFullscreen) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n requestFullscreen();\n }\n },\n [enableFullscreen, requestFullscreen]\n );\n\n if (hasError) {\n if (fallback) return <>{fallback}</>;\n return (\n <div className={className}>\n <div className=\"mv-video-error\">\n <p>Failed to load video</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n ref={inViewRef}\n className={className}\n role={enableFullscreen ? \"button\" : undefined}\n tabIndex={enableFullscreen ? 0 : undefined}\n aria-label={enableFullscreen ? \"Open video fullscreen\" : undefined}\n onDoubleClick={requestFullscreen}\n onKeyDown={onKeyDown}\n style={{ position: \"relative\", width: \"100%\", height: \"100%\", overflow: \"hidden\", outline: \"none\" }}\n >\n {showSkeleton && !isLoaded && shouldLoad && (\n <Skeleton style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }} />\n )}\n {shouldLoad && (\n <video\n ref={setVideoRef}\n src={url}\n className={mediaClassName}\n controls={controls}\n autoPlay={autoPlay}\n muted={resolvedMuted}\n loop={loop}\n poster={posterUrl}\n onLoadedData={handleLoadedData}\n onError={handleError}\n onLoadedMetadata={handleLoadedMetadata}\n onTimeUpdate={playbackHandlers.onTimeUpdate}\n onPlay={playbackHandlers.onPlay}\n onPause={playbackHandlers.onPause}\n onSeeked={playbackHandlers.onSeeked}\n onEnded={playbackHandlers.onEnded}\n preload={priority ? \"auto\" : preload}\n playsInline\n style={{\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n opacity: isLoaded ? 1 : 0,\n transition: \"opacity 300ms ease\",\n WebkitUserSelect: \"none\",\n WebkitTouchCallout: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n } as React.CSSProperties}\n />\n )}\n {endOverlay != null && hasEnded && (\n <div\n className=\"mv-end-overlay\"\n style={{ position: \"absolute\", inset: 0, zIndex: 2 }}\n >\n {endOverlay}\n </div>\n )}\n </div>\n );\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -50,6 +50,16 @@ export type MediaPlaybackProps = {
|
|
|
50
50
|
* the exact position, never suppressed. The programmatic resume seek that
|
|
51
51
|
* applies `startAtSeconds` reports as `seeked` with the resumed position.
|
|
52
52
|
* `durationSeconds` is `0` until the element's duration is known.
|
|
53
|
+
*
|
|
54
|
+
* Active-element retirement: samples come only from the ACTIVE media-element
|
|
55
|
+
* generation. When the viewer tears an element down (unload off-screen,
|
|
56
|
+
* unmount, source replacement), it retires the element first — the still-live
|
|
57
|
+
* cursor is committed once as a final `pause` sample (skipped when the last
|
|
58
|
+
* commit already reported that exact position, when playback never started,
|
|
59
|
+
* or while still inside the pre-seed suppression window) — and the reset
|
|
60
|
+
* events the teardown itself produces (browsers snap the cursor to 0 and can
|
|
61
|
+
* fire pause/seeked/timeupdate) are ignored. A genuine user seek or restart
|
|
62
|
+
* to exactly 0 on the active element still reports normally.
|
|
53
63
|
*/
|
|
54
64
|
onProgressSample?: (currentTimeSeconds: number, durationSeconds: number, reason: MediaProgressSampleReason) => void;
|
|
55
65
|
/**
|
|
@@ -131,6 +141,10 @@ export type MediaPreviewProps = {
|
|
|
131
141
|
className?: string;
|
|
132
142
|
unloadOnExit?: boolean;
|
|
133
143
|
isCircular?: boolean;
|
|
144
|
+
/** Image type only — forwarded to ImageViewer's click-to-zoom toggle (which
|
|
145
|
+
* defaults ON). Pass `false` to render a plain, non-zooming image surface.
|
|
146
|
+
* Ignored for video/audio/other types. */
|
|
147
|
+
enableZoom?: ImageViewerProps["enableZoom"];
|
|
134
148
|
priority?: boolean;
|
|
135
149
|
skeleton?: boolean;
|
|
136
150
|
lazy?: boolean;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,YAAY,EAAE,sBAAsB,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7G,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AACpE,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAE3C;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,iEAAiE;IACjE,OAAO,IAAI,IAAI,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElF;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type * as React from \"react\";\nimport type { MediaDiagnosticAdapter, AssetStatusHint } from \"./recovery.js\";\nimport type { AudioVisualizerMode } from \"./visualizer.js\";\n\nexport type { MediaDiagnosticAdapter, AssetStatusHint, DiagnosisResult, RecoveryState } from \"./recovery.js\";\nexport type { AudioVisualizerMode } from \"./visualizer.js\";\n\nexport type MediaViewerType = \"image\" | \"video\" | \"audio\" | \"other\";\nexport type FallbackMode = \"none\" | \"link\";\n\n/**\n * Imperative playback controls, exposed via the `playbackControlsRef` prop on\n * `VideoViewer` / `AudioViewer` (and forwarded by `MediaPreview` for\n * video/audio). Obtain via `React.useRef<MediaPlaybackControls>(null)`.\n */\nexport type MediaPlaybackControls = {\n /** Seek to 0 and play from the start. Clears any end-overlay. */\n restart(): void;\n /** Seek to the given position (seconds), clamped to [0, duration]. */\n seekTo(seconds: number): void;\n};\n\n/**\n * Why a progress sample was emitted. `periodic` is a throttled position report\n * during playback and is safe for a consumer to coalesce or drop; the other\n * three mark an exact moment the user's position changed and should be\n * committed as-is.\n */\nexport type MediaProgressSampleReason = \"periodic\" | \"pause\" | \"seeked\" | \"ended\";\n\n/**\n * Additive, fully optional playback API shared by `VideoViewer` and\n * `AudioViewer`. Every field is optional; when all are absent the viewers\n * behave exactly as before.\n */\nexport type MediaPlaybackProps = {\n /**\n * Fires when the media element ends (native `ended` event) with the play\n * cursor at duration. NOTE: when `loop` is set the browser restarts natively\n * and emits no `ended` event, so `onEnded` never fires in that mode.\n */\n onEnded?: () => void;\n\n /**\n * Playback-position reporting. Every sample carries the `reason` it was\n * emitted for ({@link MediaProgressSampleReason}) as an additive third\n * argument, so a consumer can throttle `periodic` reports while committing\n * the exact-moment ones. Contract:\n * - `periodic` — while playing, fires at most once every ~5 seconds of\n * playback (throttled off `timeupdate`). Never fires while paused, and is\n * SUPPRESSED near the start until the resume decision is settled: while no\n * `startAtSeconds` seed has been adopted and the element is still within\n * the late-resume adoption window, an autoplaying element reports nothing,\n * so a position of ~0 can never overwrite a saved one.\n * - `pause` / `seeked` / `ended` — fires once immediately on that event with\n * the exact position, never suppressed. The programmatic resume seek that\n * applies `startAtSeconds` reports as `seeked` with the resumed position.\n * `durationSeconds` is `0` until the element's duration is known.\n */\n onProgressSample?: (\n currentTimeSeconds: number,\n durationSeconds: number,\n reason: MediaProgressSampleReason,\n ) => void;\n\n /**\n * Initial playback position (seconds), applied once the element has metadata.\n * May arrive late (e.g. from an async prefs read that resolves after the\n * viewer mounted): a value that appears while playback has not yet begun is\n * adopted and applied to the already-loaded element. Once real playback has\n * begun a later value is ignored — it never yanks playing media.\n * Survives the `unloadOnExit` unload/remount cycle: once playback has started\n * the viewer tracks the live position internally and resumes from the LAST\n * KNOWN position on remount — it does not re-apply this prop and does not\n * restart at 0.\n */\n startAtSeconds?: number;\n\n /**\n * Rendered as an overlay covering the media area when the media has ended.\n * Removed when playback restarts (native replay or imperative `restart()`).\n * The overlay only intercepts pointer events while shown and renders above\n * the element. In a native-fullscreen exit state the overlay reappears over\n * the inline element (it is part of normal document flow, not the fullscreen\n * layer).\n */\n endOverlay?: React.ReactNode;\n\n /** Imperative controls handle — see {@link MediaPlaybackControls}. */\n playbackControlsRef?: React.Ref<MediaPlaybackControls>;\n};\n\nexport type BaseMediaProps = {\n url: string;\n className?: string;\n mediaClassName?: string;\n lazy?: boolean;\n priority?: boolean;\n skeleton?: boolean;\n onLoad?: () => void;\n onError?: () => void;\n fallback?: React.ReactNode;\n /**\n * Load-watchdog budget in ms (default LOAD_WATCHDOG_MS). A visible, loading\n * element that produces neither a load/metadata nor an error event within\n * this budget synthesizes an error so the asset resolves to a bounded\n * terminal state instead of an eternal skeleton. `0` disables.\n */\n loadTimeoutMs?: number;\n};\n\nexport type ImageViewerProps = BaseMediaProps & {\n alt?: string;\n enableZoom?: boolean;\n unloadOnExit?: boolean;\n isCircular?: boolean;\n preventGestures?: boolean;\n};\n\nexport type VideoViewerProps = BaseMediaProps & MediaPlaybackProps & {\n controls?: boolean;\n autoPlay?: boolean;\n muted?: boolean;\n loop?: boolean;\n posterUrl?: string;\n enableFullscreen?: boolean;\n unloadOnExit?: boolean;\n preload?: \"auto\" | \"metadata\" | \"none\";\n autoPlayOnVisible?: boolean;\n};\n\nexport type AudioViewerProps = BaseMediaProps & MediaPlaybackProps & {\n autoPlay?: boolean;\n loop?: boolean;\n preload?: \"auto\" | \"metadata\" | \"none\";\n onLoadChange?: (isLoading: boolean) => void;\n\n // AudioViewer always uses the package-owned control surface. Native browser\n // audio controls are intentionally not part of the public contract.\n /** Initial visualizer mode (\"line\" oscilloscope | \"bars\" EQ). A persisted user choice wins. */\n visualizerMode?: AudioVisualizerMode;\n /** localStorage key base for the minimize/mode prefs. `null` disables persistence. */\n persistKey?: string | null;\n /** App-injected action buttons rendered at the end of the player control row. */\n extraActions?: React.ReactNode;\n};\n\nexport type MediaPreviewProps = {\n url?: string | File | Blob | null;\n alt?: string;\n type?: MediaViewerType | string;\n className?: string;\n unloadOnExit?: boolean;\n isCircular?: boolean;\n priority?: boolean;\n skeleton?: boolean;\n lazy?: boolean;\n /** Video controls only. Audio always renders the canonical package-owned controls. */\n controls?: boolean;\n autoPlay?: boolean;\n muted?: boolean;\n loop?: boolean;\n posterUrl?: string;\n preload?: \"auto\" | \"metadata\" | \"none\";\n autoPlayOnVisible?: boolean;\n onLoad?: () => void;\n onError?: () => void;\n onLoadChange?: (isLoading: boolean) => void;\n\n // -------------------------------------------------------------------------\n // Playback API (additive, optional) — forwarded to the video/audio viewer.\n // Ignored for image/other types. See MediaPlaybackProps for the contract.\n // -------------------------------------------------------------------------\n onEnded?: MediaPlaybackProps[\"onEnded\"];\n onProgressSample?: MediaPlaybackProps[\"onProgressSample\"];\n startAtSeconds?: MediaPlaybackProps[\"startAtSeconds\"];\n endOverlay?: MediaPlaybackProps[\"endOverlay\"];\n playbackControlsRef?: MediaPlaybackProps[\"playbackControlsRef\"];\n\n // Audio always uses the package-owned player. These props customize that\n // single control surface and are ignored for every other media type.\n audioVisualizerMode?: AudioVisualizerMode;\n audioPersistKey?: string | null;\n audioExtraActions?: React.ReactNode;\n\n fallbackMode?: FallbackMode;\n fallbackLabel?: string;\n filename?: string;\n mime?: string;\n name?: string;\n\n // -------------------------------------------------------------------------\n // Recovery adapter (additive, optional — backward-compatible)\n // -------------------------------------------------------------------------\n /**\n * Injected diagnostic adapter. When provided, element load errors for\n * assets the app believes are live will trigger the bounded recovery state\n * machine. When omitted, the existing error fallback behavior is unchanged.\n */\n recoveryAdapter?: MediaDiagnosticAdapter;\n\n /**\n * Optional app hint about the server-side lifecycle state of the asset.\n * Drives which recovery phase is shown (e.g. processing → skeleton,\n * finalizing → spinner, failed/rejected → hard error immediately).\n */\n assetStatusHint?: AssetStatusHint;\n\n /** Load-watchdog budget forwarded to the underlying viewer — see BaseMediaProps. */\n loadTimeoutMs?: number;\n};\n\n/** Back-compat alias */\nexport type MediaViewerProps = MediaPreviewProps;\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type * as React from \"react\";\nimport type { MediaDiagnosticAdapter, AssetStatusHint } from \"./recovery.js\";\nimport type { AudioVisualizerMode } from \"./visualizer.js\";\n\nexport type { MediaDiagnosticAdapter, AssetStatusHint, DiagnosisResult, RecoveryState } from \"./recovery.js\";\nexport type { AudioVisualizerMode } from \"./visualizer.js\";\n\nexport type MediaViewerType = \"image\" | \"video\" | \"audio\" | \"other\";\nexport type FallbackMode = \"none\" | \"link\";\n\n/**\n * Imperative playback controls, exposed via the `playbackControlsRef` prop on\n * `VideoViewer` / `AudioViewer` (and forwarded by `MediaPreview` for\n * video/audio). Obtain via `React.useRef<MediaPlaybackControls>(null)`.\n */\nexport type MediaPlaybackControls = {\n /** Seek to 0 and play from the start. Clears any end-overlay. */\n restart(): void;\n /** Seek to the given position (seconds), clamped to [0, duration]. */\n seekTo(seconds: number): void;\n};\n\n/**\n * Why a progress sample was emitted. `periodic` is a throttled position report\n * during playback and is safe for a consumer to coalesce or drop; the other\n * three mark an exact moment the user's position changed and should be\n * committed as-is.\n */\nexport type MediaProgressSampleReason = \"periodic\" | \"pause\" | \"seeked\" | \"ended\";\n\n/**\n * Additive, fully optional playback API shared by `VideoViewer` and\n * `AudioViewer`. Every field is optional; when all are absent the viewers\n * behave exactly as before.\n */\nexport type MediaPlaybackProps = {\n /**\n * Fires when the media element ends (native `ended` event) with the play\n * cursor at duration. NOTE: when `loop` is set the browser restarts natively\n * and emits no `ended` event, so `onEnded` never fires in that mode.\n */\n onEnded?: () => void;\n\n /**\n * Playback-position reporting. Every sample carries the `reason` it was\n * emitted for ({@link MediaProgressSampleReason}) as an additive third\n * argument, so a consumer can throttle `periodic` reports while committing\n * the exact-moment ones. Contract:\n * - `periodic` — while playing, fires at most once every ~5 seconds of\n * playback (throttled off `timeupdate`). Never fires while paused, and is\n * SUPPRESSED near the start until the resume decision is settled: while no\n * `startAtSeconds` seed has been adopted and the element is still within\n * the late-resume adoption window, an autoplaying element reports nothing,\n * so a position of ~0 can never overwrite a saved one.\n * - `pause` / `seeked` / `ended` — fires once immediately on that event with\n * the exact position, never suppressed. The programmatic resume seek that\n * applies `startAtSeconds` reports as `seeked` with the resumed position.\n * `durationSeconds` is `0` until the element's duration is known.\n *\n * Active-element retirement: samples come only from the ACTIVE media-element\n * generation. When the viewer tears an element down (unload off-screen,\n * unmount, source replacement), it retires the element first — the still-live\n * cursor is committed once as a final `pause` sample (skipped when the last\n * commit already reported that exact position, when playback never started,\n * or while still inside the pre-seed suppression window) — and the reset\n * events the teardown itself produces (browsers snap the cursor to 0 and can\n * fire pause/seeked/timeupdate) are ignored. A genuine user seek or restart\n * to exactly 0 on the active element still reports normally.\n */\n onProgressSample?: (\n currentTimeSeconds: number,\n durationSeconds: number,\n reason: MediaProgressSampleReason,\n ) => void;\n\n /**\n * Initial playback position (seconds), applied once the element has metadata.\n * May arrive late (e.g. from an async prefs read that resolves after the\n * viewer mounted): a value that appears while playback has not yet begun is\n * adopted and applied to the already-loaded element. Once real playback has\n * begun a later value is ignored — it never yanks playing media.\n * Survives the `unloadOnExit` unload/remount cycle: once playback has started\n * the viewer tracks the live position internally and resumes from the LAST\n * KNOWN position on remount — it does not re-apply this prop and does not\n * restart at 0.\n */\n startAtSeconds?: number;\n\n /**\n * Rendered as an overlay covering the media area when the media has ended.\n * Removed when playback restarts (native replay or imperative `restart()`).\n * The overlay only intercepts pointer events while shown and renders above\n * the element. In a native-fullscreen exit state the overlay reappears over\n * the inline element (it is part of normal document flow, not the fullscreen\n * layer).\n */\n endOverlay?: React.ReactNode;\n\n /** Imperative controls handle — see {@link MediaPlaybackControls}. */\n playbackControlsRef?: React.Ref<MediaPlaybackControls>;\n};\n\nexport type BaseMediaProps = {\n url: string;\n className?: string;\n mediaClassName?: string;\n lazy?: boolean;\n priority?: boolean;\n skeleton?: boolean;\n onLoad?: () => void;\n onError?: () => void;\n fallback?: React.ReactNode;\n /**\n * Load-watchdog budget in ms (default LOAD_WATCHDOG_MS). A visible, loading\n * element that produces neither a load/metadata nor an error event within\n * this budget synthesizes an error so the asset resolves to a bounded\n * terminal state instead of an eternal skeleton. `0` disables.\n */\n loadTimeoutMs?: number;\n};\n\nexport type ImageViewerProps = BaseMediaProps & {\n alt?: string;\n enableZoom?: boolean;\n unloadOnExit?: boolean;\n isCircular?: boolean;\n preventGestures?: boolean;\n};\n\nexport type VideoViewerProps = BaseMediaProps & MediaPlaybackProps & {\n controls?: boolean;\n autoPlay?: boolean;\n muted?: boolean;\n loop?: boolean;\n posterUrl?: string;\n enableFullscreen?: boolean;\n unloadOnExit?: boolean;\n preload?: \"auto\" | \"metadata\" | \"none\";\n autoPlayOnVisible?: boolean;\n};\n\nexport type AudioViewerProps = BaseMediaProps & MediaPlaybackProps & {\n autoPlay?: boolean;\n loop?: boolean;\n preload?: \"auto\" | \"metadata\" | \"none\";\n onLoadChange?: (isLoading: boolean) => void;\n\n // AudioViewer always uses the package-owned control surface. Native browser\n // audio controls are intentionally not part of the public contract.\n /** Initial visualizer mode (\"line\" oscilloscope | \"bars\" EQ). A persisted user choice wins. */\n visualizerMode?: AudioVisualizerMode;\n /** localStorage key base for the minimize/mode prefs. `null` disables persistence. */\n persistKey?: string | null;\n /** App-injected action buttons rendered at the end of the player control row. */\n extraActions?: React.ReactNode;\n};\n\nexport type MediaPreviewProps = {\n url?: string | File | Blob | null;\n alt?: string;\n type?: MediaViewerType | string;\n className?: string;\n unloadOnExit?: boolean;\n isCircular?: boolean;\n /** Image type only — forwarded to ImageViewer's click-to-zoom toggle (which\n * defaults ON). Pass `false` to render a plain, non-zooming image surface.\n * Ignored for video/audio/other types. */\n enableZoom?: ImageViewerProps[\"enableZoom\"];\n priority?: boolean;\n skeleton?: boolean;\n lazy?: boolean;\n /** Video controls only. Audio always renders the canonical package-owned controls. */\n controls?: boolean;\n autoPlay?: boolean;\n muted?: boolean;\n loop?: boolean;\n posterUrl?: string;\n preload?: \"auto\" | \"metadata\" | \"none\";\n autoPlayOnVisible?: boolean;\n onLoad?: () => void;\n onError?: () => void;\n onLoadChange?: (isLoading: boolean) => void;\n\n // -------------------------------------------------------------------------\n // Playback API (additive, optional) — forwarded to the video/audio viewer.\n // Ignored for image/other types. See MediaPlaybackProps for the contract.\n // -------------------------------------------------------------------------\n onEnded?: MediaPlaybackProps[\"onEnded\"];\n onProgressSample?: MediaPlaybackProps[\"onProgressSample\"];\n startAtSeconds?: MediaPlaybackProps[\"startAtSeconds\"];\n endOverlay?: MediaPlaybackProps[\"endOverlay\"];\n playbackControlsRef?: MediaPlaybackProps[\"playbackControlsRef\"];\n\n // Audio always uses the package-owned player. These props customize that\n // single control surface and are ignored for every other media type.\n audioVisualizerMode?: AudioVisualizerMode;\n audioPersistKey?: string | null;\n audioExtraActions?: React.ReactNode;\n\n fallbackMode?: FallbackMode;\n fallbackLabel?: string;\n filename?: string;\n mime?: string;\n name?: string;\n\n // -------------------------------------------------------------------------\n // Recovery adapter (additive, optional — backward-compatible)\n // -------------------------------------------------------------------------\n /**\n * Injected diagnostic adapter. When provided, element load errors for\n * assets the app believes are live will trigger the bounded recovery state\n * machine. When omitted, the existing error fallback behavior is unchanged.\n */\n recoveryAdapter?: MediaDiagnosticAdapter;\n\n /**\n * Optional app hint about the server-side lifecycle state of the asset.\n * Drives which recovery phase is shown (e.g. processing → skeleton,\n * finalizing → spinner, failed/rejected → hard error immediately).\n */\n assetStatusHint?: AssetStatusHint;\n\n /** Load-watchdog budget forwarded to the underlying viewer — see BaseMediaProps. */\n loadTimeoutMs?: number;\n};\n\n/** Back-compat alias */\nexport type MediaViewerProps = MediaPreviewProps;\n"]}
|