@ttt-productions/media-viewer 0.10.5 → 0.10.7

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.
@@ -12,7 +12,7 @@ export function AudioViewer(props) {
12
12
  const [hasMetadata, setHasMetadata] = React.useState(false);
13
13
  const [shouldLoad, setShouldLoad] = React.useState(priority || !lazy);
14
14
  const audioRef = React.useRef(null);
15
- // Additive playback API — derives from element events (no observer, Rule 22).
15
+ // Additive playback API — derives from element events (no observer, MEDIA-102).
16
16
  const { hasEnded, handlers: playbackHandlers } = useMediaPlayback(audioRef, { onEnded, onProgressSample, startAtSeconds, endOverlay }, playbackControlsRef, url);
17
17
  const { ref: inViewRef, inView } = useInView({
18
18
  triggerOnce: false,
@@ -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, Rule 22).\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 } = 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"]}
@@ -10,7 +10,7 @@
10
10
  * period instead of failing the asset.
11
11
  *
12
12
  * `timeoutMs <= 0` disables the watchdog. No IntersectionObserver is used
13
- * (Rule 22) — visibility gating comes from the caller's `armed` flag, which
13
+ * (MEDIA-102) — visibility gating comes from the caller's `armed` flag, which
14
14
  * viewers derive from their existing single observer's `shouldLoad`.
15
15
  */
16
16
  export declare function useLoadWatchdog(armed: boolean, timeoutMs: number | undefined, onTimeout: () => void): void;
@@ -13,7 +13,7 @@ import { LOAD_WATCHDOG_MS } from "../recovery.js";
13
13
  * period instead of failing the asset.
14
14
  *
15
15
  * `timeoutMs <= 0` disables the watchdog. No IntersectionObserver is used
16
- * (Rule 22) — visibility gating comes from the caller's `armed` flag, which
16
+ * (MEDIA-102) — visibility gating comes from the caller's `armed` flag, which
17
17
  * viewers derive from their existing single observer's `shouldLoad`.
18
18
  */
19
19
  export function useLoadWatchdog(armed, timeoutMs, onTimeout) {
@@ -1 +1 @@
1
- {"version":3,"file":"use-load-watchdog.js","sourceRoot":"","sources":["../../src/react/use-load-watchdog.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAc,EACd,SAA6B,EAC7B,SAAqB;IAErB,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC7C,YAAY,CAAC,OAAO,GAAG,SAAS,CAAC;IAEjC,MAAM,eAAe,GAAG,SAAS,IAAI,gBAAgB,CAAC;IAEtD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,KAAK,IAAI,eAAe,IAAI,CAAC;YAAE,OAAO;QAE3C,IAAI,KAAK,GAAyC,IAAI,CAAC;QAEvD,MAAM,GAAG,GAAG,GAAG,EAAE;YACf,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBACtB,KAAK,GAAG,IAAI,CAAC;gBACb,IACE,OAAO,QAAQ,KAAK,WAAW;oBAC/B,QAAQ,CAAC,eAAe,KAAK,QAAQ,EACrC,CAAC;oBACD,GAAG,EAAE,CAAC;oBACN,OAAO;gBACT,CAAC;gBACD,YAAY,CAAC,OAAO,EAAE,CAAC;YACzB,CAAC,EAAE,eAAe,CAAC,CAAC;QACtB,CAAC,CAAC;QAEF,GAAG,EAAE,CAAC;QAEN,OAAO,GAAG,EAAE;YACV,IAAI,KAAK,KAAK,IAAI;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { LOAD_WATCHDOG_MS } from \"../recovery.js\";\n\n/**\n * Bounded load watchdog shared by the three viewers. While `armed`, a media\n * element is expected to produce a load (or at least metadata) or error event\n * within `timeoutMs`; when neither arrives, `onTimeout` fires so the viewer\n * can synthesize an error and hand the asset to the bounded recovery path —\n * no viewer may sit on a skeleton forever.\n *\n * A hidden document pauses the deadline (browsers deprioritize background\n * loads): if the timer fires while the tab is hidden, it re-arms for a full\n * period instead of failing the asset.\n *\n * `timeoutMs <= 0` disables the watchdog. No IntersectionObserver is used\n * (Rule 22) — visibility gating comes from the caller's `armed` flag, which\n * viewers derive from their existing single observer's `shouldLoad`.\n */\nexport function useLoadWatchdog(\n armed: boolean,\n timeoutMs: number | undefined,\n onTimeout: () => void\n): void {\n const onTimeoutRef = React.useRef(onTimeout);\n onTimeoutRef.current = onTimeout;\n\n const resolvedTimeout = timeoutMs ?? LOAD_WATCHDOG_MS;\n\n React.useEffect(() => {\n if (!armed || resolvedTimeout <= 0) return;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const arm = () => {\n timer = setTimeout(() => {\n timer = null;\n if (\n typeof document !== \"undefined\" &&\n document.visibilityState === \"hidden\"\n ) {\n arm();\n return;\n }\n onTimeoutRef.current();\n }, resolvedTimeout);\n };\n\n arm();\n\n return () => {\n if (timer !== null) clearTimeout(timer);\n };\n }, [armed, resolvedTimeout]);\n}\n"]}
1
+ {"version":3,"file":"use-load-watchdog.js","sourceRoot":"","sources":["../../src/react/use-load-watchdog.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAc,EACd,SAA6B,EAC7B,SAAqB;IAErB,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC7C,YAAY,CAAC,OAAO,GAAG,SAAS,CAAC;IAEjC,MAAM,eAAe,GAAG,SAAS,IAAI,gBAAgB,CAAC;IAEtD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,KAAK,IAAI,eAAe,IAAI,CAAC;YAAE,OAAO;QAE3C,IAAI,KAAK,GAAyC,IAAI,CAAC;QAEvD,MAAM,GAAG,GAAG,GAAG,EAAE;YACf,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBACtB,KAAK,GAAG,IAAI,CAAC;gBACb,IACE,OAAO,QAAQ,KAAK,WAAW;oBAC/B,QAAQ,CAAC,eAAe,KAAK,QAAQ,EACrC,CAAC;oBACD,GAAG,EAAE,CAAC;oBACN,OAAO;gBACT,CAAC;gBACD,YAAY,CAAC,OAAO,EAAE,CAAC;YACzB,CAAC,EAAE,eAAe,CAAC,CAAC;QACtB,CAAC,CAAC;QAEF,GAAG,EAAE,CAAC;QAEN,OAAO,GAAG,EAAE;YACV,IAAI,KAAK,KAAK,IAAI;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { LOAD_WATCHDOG_MS } from \"../recovery.js\";\n\n/**\n * Bounded load watchdog shared by the three viewers. While `armed`, a media\n * element is expected to produce a load (or at least metadata) or error event\n * within `timeoutMs`; when neither arrives, `onTimeout` fires so the viewer\n * can synthesize an error and hand the asset to the bounded recovery path —\n * no viewer may sit on a skeleton forever.\n *\n * A hidden document pauses the deadline (browsers deprioritize background\n * loads): if the timer fires while the tab is hidden, it re-arms for a full\n * period instead of failing the asset.\n *\n * `timeoutMs <= 0` disables the watchdog. No IntersectionObserver is used\n * (MEDIA-102) — visibility gating comes from the caller's `armed` flag, which\n * viewers derive from their existing single observer's `shouldLoad`.\n */\nexport function useLoadWatchdog(\n armed: boolean,\n timeoutMs: number | undefined,\n onTimeout: () => void\n): void {\n const onTimeoutRef = React.useRef(onTimeout);\n onTimeoutRef.current = onTimeout;\n\n const resolvedTimeout = timeoutMs ?? LOAD_WATCHDOG_MS;\n\n React.useEffect(() => {\n if (!armed || resolvedTimeout <= 0) return;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const arm = () => {\n timer = setTimeout(() => {\n timer = null;\n if (\n typeof document !== \"undefined\" &&\n document.visibilityState === \"hidden\"\n ) {\n arm();\n return;\n }\n onTimeoutRef.current();\n }, resolvedTimeout);\n };\n\n arm();\n\n return () => {\n if (timer !== null) clearTimeout(timer);\n };\n }, [armed, resolvedTimeout]);\n}\n"]}
@@ -22,7 +22,7 @@ export type UseMediaPlaybackResult = {
22
22
  * Shared playback API for VideoViewer and AudioViewer. Owns the additive
23
23
  * playback surface (onEnded, onProgressSample, startAtSeconds, endOverlay,
24
24
  * imperative controls) so both viewers behave identically. Adds NO observer —
25
- * everything is derived from native media element events (Rule 22).
25
+ * everything is derived from native media element events (MEDIA-102).
26
26
  *
27
27
  * The element ref is supplied by the viewer (which already owns it for
28
28
  * autoplay/fullscreen). When the viewer unloads the element off-screen
@@ -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,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE7E;;;GAGG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD,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;CACH,CAAC;AAEF;;;;;;;;;;;;GAYG;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,CA4LxB"}
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,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE7E;;;GAGG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD,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;CACH,CAAC;AAEF;;;;;;;;;;;;GAYG;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,CAoNxB"}
@@ -9,7 +9,7 @@ export const PROGRESS_SAMPLE_INTERVAL_MS = 5000;
9
9
  * Shared playback API for VideoViewer and AudioViewer. Owns the additive
10
10
  * playback surface (onEnded, onProgressSample, startAtSeconds, endOverlay,
11
11
  * imperative controls) so both viewers behave identically. Adds NO observer —
12
- * everything is derived from native media element events (Rule 22).
12
+ * everything is derived from native media element events (MEDIA-102).
13
13
  *
14
14
  * The element ref is supplied by the viewer (which already owns it for
15
15
  * autoplay/fullscreen). When the viewer unloads the element off-screen
@@ -35,11 +35,14 @@ mediaKey) {
35
35
  const onProgressSampleRef = React.useRef(onProgressSample);
36
36
  onEndedRef.current = onEnded;
37
37
  onProgressSampleRef.current = onProgressSample;
38
- const seedPosition = () => typeof startAtSeconds === "number" && startAtSeconds > 0 ? startAtSeconds : null;
38
+ // The resume position implied by the CURRENT props: a positive
39
+ // startAtSeconds, or null when the caller supplies none. One declaration —
40
+ // the mount seed, the mediaKey reset, and the late-arrival effect all read it.
41
+ const seedFromProps = typeof startAtSeconds === "number" && startAtSeconds > 0 ? startAtSeconds : null;
39
42
  // Last known playback position. Seeded from startAtSeconds, then updated once
40
43
  // playback has actually started. Survives the unloadOnExit unload/remount
41
44
  // cycle because it lives on the (still-mounted) component instance.
42
- const lastPositionRef = React.useRef(seedPosition());
45
+ const lastPositionRef = React.useRef(seedFromProps);
43
46
  // True once we have observed real playback progress — after this we resume
44
47
  // from lastPositionRef, never re-apply the original startAtSeconds prop.
45
48
  const hasStartedRef = React.useRef(false);
@@ -54,7 +57,7 @@ mediaKey) {
54
57
  const prevMediaKeyRef = React.useRef(mediaKey);
55
58
  if (prevMediaKeyRef.current !== mediaKey) {
56
59
  prevMediaKeyRef.current = mediaKey;
57
- lastPositionRef.current = seedPosition();
60
+ lastPositionRef.current = seedFromProps;
58
61
  hasStartedRef.current = false;
59
62
  lastSampleAtRef.current = 0;
60
63
  if (hasEnded)
@@ -91,6 +94,31 @@ mediaKey) {
91
94
  /* setting currentTime before ready can throw in some engines; ignore */
92
95
  }
93
96
  }, []);
97
+ // Late-arriving resume position. `startAtSeconds` usually comes from an async
98
+ // app source (a user-prefs query); when that resolves AFTER this hook mounted,
99
+ // the mount-time seed above saw `undefined` and the element would silently
100
+ // start at 0. Adopt the value when it finally arrives — but ONLY while nothing
101
+ // has been seeded, no real playback has begun, and the element is still at the
102
+ // start, so a late value can never yank media the user is already watching and
103
+ // can never overwrite a truthful tracked position. A `mediaKey` change resets
104
+ // both refs in the render-phase block above, which re-arms this for the new
105
+ // source; the seek itself is applied here because the element may already have
106
+ // fired `loadedmetadata` (that is exactly this defect) and will not fire it
107
+ // again — when metadata has not landed yet, `onLoadedMetadata` applies it.
108
+ React.useEffect(() => {
109
+ if (seedFromProps === null)
110
+ return;
111
+ if (hasStartedRef.current)
112
+ return;
113
+ if (lastPositionRef.current !== null)
114
+ return;
115
+ const el = elementRef.current;
116
+ if (el && el.currentTime > 0.01)
117
+ return;
118
+ lastPositionRef.current = seedFromProps;
119
+ if (el)
120
+ applyInitialSeek(el);
121
+ }, [seedFromProps, applyInitialSeek, elementRef]);
94
122
  const onLoadedMetadata = React.useCallback((e) => {
95
123
  applyInitialSeek(e.currentTarget);
96
124
  }, [applyInitialSeek]);
@@ -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 { MediaPlaybackControls, MediaPlaybackProps } 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\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 (Rule 22).\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 const seedPosition = () =>\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>(seedPosition());\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\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 = seedPosition();\n hasStartedRef.current = false;\n lastSampleAtRef.current = 0;\n if (hasEnded) setHasEnded(false);\n }\n\n const emitSample = React.useCallback((el: HTMLMediaElement) => {\n const cb = onProgressSampleRef.current;\n if (!cb) return;\n const duration = Number.isFinite(el.duration) ? el.duration : 0;\n cb(el.currentTime, duration);\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) => {\n const target = lastPositionRef.current;\n if (typeof target !== \"number\" || target <= 0) return;\n // Only seek a freshly-loaded element sitting at the start. This makes the\n // handler idempotent (a metadata event on an already-positioned element is\n // a no-op) and, crucially, re-applies the LAST KNOWN position to the new\n // element created by an unloadOnExit remount — resuming, never restarting.\n if (el.currentTime > 0.01) 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 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 const now = Date.now();\n if (now - lastSampleAtRef.current >= PROGRESS_SAMPLE_INTERVAL_MS) {\n lastSampleAtRef.current = now;\n emitSample(el);\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);\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 lastSampleAtRef.current = Date.now();\n emitSample(el);\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);\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 { MediaPlaybackControls, MediaPlaybackProps } 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\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\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 lastSampleAtRef.current = 0;\n if (hasEnded) setHasEnded(false);\n }\n\n const emitSample = React.useCallback((el: HTMLMediaElement) => {\n const cb = onProgressSampleRef.current;\n if (!cb) return;\n const duration = Number.isFinite(el.duration) ? el.duration : 0;\n cb(el.currentTime, duration);\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) => {\n const target = lastPositionRef.current;\n if (typeof target !== \"number\" || target <= 0) return;\n // Only seek a freshly-loaded element sitting at the start. This makes the\n // handler idempotent (a metadata event on an already-positioned element is\n // a no-op) and, crucially, re-applies the LAST KNOWN position to the new\n // element created by an unloadOnExit remount — resuming, never restarting.\n if (el.currentTime > 0.01) 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 — but ONLY while nothing\n // has been seeded, no real playback has begun, and the element is still at the\n // start, so a late value can never yank media the user is already watching and\n // can never overwrite a truthful tracked position. A `mediaKey` change resets\n // both refs in the render-phase block above, which re-arms this for the new\n // source; the seek itself is applied here because the element may already have\n // fired `loadedmetadata` (that is exactly this defect) and will not fire it\n // again — when metadata has not landed yet, `onLoadedMetadata` applies it.\n React.useEffect(() => {\n if (seedFromProps === null) return;\n if (hasStartedRef.current) return;\n if (lastPositionRef.current !== null) return;\n const el = elementRef.current;\n if (el && el.currentTime > 0.01) return;\n lastPositionRef.current = seedFromProps;\n if (el) applyInitialSeek(el);\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 const now = Date.now();\n if (now - lastSampleAtRef.current >= PROGRESS_SAMPLE_INTERVAL_MS) {\n lastSampleAtRef.current = now;\n emitSample(el);\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);\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 lastSampleAtRef.current = Date.now();\n emitSample(el);\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);\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"]}
@@ -15,7 +15,7 @@ export function VideoViewer(props) {
15
15
  const videoRef = React.useRef(null);
16
16
  // Additive playback API (onEnded, progress sampling, startAt/resume,
17
17
  // endOverlay, imperative controls). Derives everything from element events —
18
- // adds no observer (Rule 22).
18
+ // adds no observer (MEDIA-102).
19
19
  const { hasEnded, handlers: playbackHandlers } = useMediaPlayback(videoRef, { onEnded, onProgressSample, startAtSeconds, endOverlay }, playbackControlsRef, url);
20
20
  // Single observer for both lazy-load gating AND autoplay-on-visible.
21
21
  // threshold: [0, 0.5] — 0 fires "any pixel visible" (drives shouldLoad),
@@ -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 (Rule 22).\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 } = 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"]}
package/dist/types.d.ts CHANGED
@@ -40,6 +40,10 @@ export type MediaPlaybackProps = {
40
40
  onProgressSample?: (currentTimeSeconds: number, durationSeconds: number) => void;
41
41
  /**
42
42
  * Initial playback position (seconds), applied once the element has metadata.
43
+ * May arrive late (e.g. from an async prefs read that resolves after the
44
+ * viewer mounted): a value that appears while playback has not yet begun is
45
+ * adopted and applied to the already-loaded element. Once real playback has
46
+ * begun a later value is ignored — it never yanks playing media.
43
47
  * Survives the `unloadOnExit` unload/remount cycle: once playback has started
44
48
  * the viewer tracks the live position internally and resumes from the LAST
45
49
  * KNOWN position on remount — it does not re-apply this prop and does not
@@ -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;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,CAAC,kBAAkB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjF;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE7B,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG;IAC9C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,kBAAkB,GAAG;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,kBAAkB,GAAG;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACvC,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAI5C,+FAA+F;IAC/F,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iFAAiF;IACjF,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,sFAAsF;IACtF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAM5C,OAAO,CAAC,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;IAC1D,cAAc,CAAC,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;IACtD,UAAU,CAAC,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAC9C,mBAAmB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;IAIhE,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAEpC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IAKd;;;;OAIG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAC;IAEzC;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAElC,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,wBAAwB;AACxB,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,CAAC"}
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 * 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 * Periodic playback-position reporting. Contract:\n * - While playing, fires at most once every ~5 seconds of playback\n * (throttled off `timeupdate`).\n * - Fires once immediately on pause, on seek completion (`seeked`), and on\n * ended — each carrying the exact position at that moment.\n * - Never fires while paused (aside from the single pause-flush above).\n * `durationSeconds` is `0` until the element's duration is known.\n */\n onProgressSample?: (currentTimeSeconds: number, durationSeconds: number) => void;\n\n /**\n * Initial playback position (seconds), applied once the element has metadata.\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 * 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 * Periodic playback-position reporting. Contract:\n * - While playing, fires at most once every ~5 seconds of playback\n * (throttled off `timeupdate`).\n * - Fires once immediately on pause, on seek completion (`seeked`), and on\n * ended — each carrying the exact position at that moment.\n * - Never fires while paused (aside from the single pause-flush above).\n * `durationSeconds` is `0` until the element's duration is known.\n */\n onProgressSample?: (currentTimeSeconds: number, durationSeconds: number) => 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/media-viewer",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",
@@ -33,8 +33,8 @@
33
33
  "prepublishOnly": "npm run clean && npm run build"
34
34
  },
35
35
  "dependencies": {
36
- "@ttt-productions/media-schemas": "^0.14.2",
37
- "@ttt-productions/ui-core": "^0.11.8"
36
+ "@ttt-productions/media-schemas": "^0.14.4",
37
+ "@ttt-productions/ui-core": "^0.11.12"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": ">=19.0.0",