@ttt-productions/media-viewer 0.10.4 → 0.10.6

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/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export type { AudioVisualizerMode, MediaViewerType, FallbackMode, BaseMediaProps
2
2
  export { BACKOFF_SCHEDULE_MS, MAX_RECOVERY_DURATION_MS, LOAD_WATCHDOG_MS, PHASE_MAX_WAIT_MS, applyJitter, backoffForAttempt, withinBudget, } from "./recovery.js";
3
3
  export { startWaveformLoop, drawIdleFrame } from "./visualizer.js";
4
4
  export type { StartWaveformLoopOptions } from "./visualizer.js";
5
+ export { resolveInfiniteDuration } from "./media-duration.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,EACf,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnE,YAAY,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,EACf,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnE,YAAY,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { BACKOFF_SCHEDULE_MS, MAX_RECOVERY_DURATION_MS, LOAD_WATCHDOG_MS, PHASE_MAX_WAIT_MS, applyJitter, backoffForAttempt, withinBudget, } from "./recovery.js";
2
2
  export { startWaveformLoop, drawIdleFrame } from "./visualizer.js";
3
+ export { resolveInfiniteDuration } from "./media-duration.js";
3
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["export type {\n AudioVisualizerMode,\n MediaViewerType,\n FallbackMode,\n BaseMediaProps,\n ImageViewerProps,\n VideoViewerProps,\n AudioViewerProps,\n MediaPreviewProps,\n MediaViewerProps,\n MediaPlaybackProps,\n MediaPlaybackControls,\n MediaDiagnosticAdapter,\n AssetStatusHint,\n DiagnosisResult,\n RecoveryState,\n} from \"./types.js\";\n\nexport {\n BACKOFF_SCHEDULE_MS,\n MAX_RECOVERY_DURATION_MS,\n LOAD_WATCHDOG_MS,\n PHASE_MAX_WAIT_MS,\n applyJitter,\n backoffForAttempt,\n withinBudget,\n} from \"./recovery.js\";\n\nexport { startWaveformLoop, drawIdleFrame } from \"./visualizer.js\";\nexport type { StartWaveformLoopOptions } from \"./visualizer.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["export type {\n AudioVisualizerMode,\n MediaViewerType,\n FallbackMode,\n BaseMediaProps,\n ImageViewerProps,\n VideoViewerProps,\n AudioViewerProps,\n MediaPreviewProps,\n MediaViewerProps,\n MediaPlaybackProps,\n MediaPlaybackControls,\n MediaDiagnosticAdapter,\n AssetStatusHint,\n DiagnosisResult,\n RecoveryState,\n} from \"./types.js\";\n\nexport {\n BACKOFF_SCHEDULE_MS,\n MAX_RECOVERY_DURATION_MS,\n LOAD_WATCHDOG_MS,\n PHASE_MAX_WAIT_MS,\n applyJitter,\n backoffForAttempt,\n withinBudget,\n} from \"./recovery.js\";\n\nexport { startWaveformLoop, drawIdleFrame } from \"./visualizer.js\";\nexport type { StartWaveformLoopOptions } from \"./visualizer.js\";\n\nexport { resolveInfiniteDuration } from \"./media-duration.js\";\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Chromium MediaRecorder blobs (webm/opus audio, webm video) report
3
+ * `duration: Infinity` (or NaN) at `loadedmetadata` — the webm header carries
4
+ * no duration until the engine has scanned the stream. The standard workaround:
5
+ * seek far past the end; the engine then computes the real duration and fires
6
+ * `durationchange` with a finite value.
7
+ *
8
+ * Resolves the element's REAL finite duration in seconds, or `null` when it
9
+ * cannot be determined within `timeoutMs` — callers decide their own fallback
10
+ * (the file-input duration validator fails OPEN on null; the audio player
11
+ * chrome keeps its placeholder). When the workaround seek actually ran,
12
+ * `currentTime` is restored to 0 before resolving, so a paused just-mounted
13
+ * preview is left visually untouched.
14
+ */
15
+ export declare function resolveInfiniteDuration(el: HTMLMediaElement, timeoutMs?: number): Promise<number | null>;
16
+ //# sourceMappingURL=media-duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-duration.d.ts","sourceRoot":"","sources":["../src/media-duration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,gBAAgB,EACpB,SAAS,SAAO,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2BxB"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Chromium MediaRecorder blobs (webm/opus audio, webm video) report
3
+ * `duration: Infinity` (or NaN) at `loadedmetadata` — the webm header carries
4
+ * no duration until the engine has scanned the stream. The standard workaround:
5
+ * seek far past the end; the engine then computes the real duration and fires
6
+ * `durationchange` with a finite value.
7
+ *
8
+ * Resolves the element's REAL finite duration in seconds, or `null` when it
9
+ * cannot be determined within `timeoutMs` — callers decide their own fallback
10
+ * (the file-input duration validator fails OPEN on null; the audio player
11
+ * chrome keeps its placeholder). When the workaround seek actually ran,
12
+ * `currentTime` is restored to 0 before resolving, so a paused just-mounted
13
+ * preview is left visually untouched.
14
+ */
15
+ export function resolveInfiniteDuration(el, timeoutMs = 3000) {
16
+ if (Number.isFinite(el.duration))
17
+ return Promise.resolve(el.duration);
18
+ return new Promise((resolve) => {
19
+ let done = false;
20
+ const finish = (value) => {
21
+ if (done)
22
+ return;
23
+ done = true;
24
+ el.removeEventListener("durationchange", onDurationChange);
25
+ clearTimeout(timer);
26
+ try {
27
+ el.currentTime = 0; // undo the workaround seek
28
+ }
29
+ catch {
30
+ // detached/errored element — the resolved value still stands
31
+ }
32
+ resolve(value);
33
+ };
34
+ const onDurationChange = () => {
35
+ if (Number.isFinite(el.duration))
36
+ finish(el.duration);
37
+ };
38
+ const timer = setTimeout(() => finish(null), timeoutMs);
39
+ el.addEventListener("durationchange", onDurationChange);
40
+ try {
41
+ el.currentTime = Number.MAX_SAFE_INTEGER;
42
+ }
43
+ catch {
44
+ finish(null);
45
+ }
46
+ });
47
+ }
48
+ //# sourceMappingURL=media-duration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-duration.js","sourceRoot":"","sources":["../src/media-duration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CACrC,EAAoB,EACpB,SAAS,GAAG,IAAI;IAEhB,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;IACtE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,IAAI,GAAG,KAAK,CAAC;QACjB,MAAM,MAAM,GAAG,CAAC,KAAoB,EAAE,EAAE;YACtC,IAAI,IAAI;gBAAE,OAAO;YACjB,IAAI,GAAG,IAAI,CAAC;YACZ,EAAE,CAAC,mBAAmB,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;YAC3D,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,CAAC;gBACH,EAAE,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,2BAA2B;YACjD,CAAC;YAAC,MAAM,CAAC;gBACP,6DAA6D;YAC/D,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;QACF,MAAM,gBAAgB,GAAG,GAAG,EAAE;YAC5B,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC;gBAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;QACxD,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;QACxD,EAAE,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;QACxD,IAAI,CAAC;YACH,EAAE,CAAC,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Chromium MediaRecorder blobs (webm/opus audio, webm video) report\n * `duration: Infinity` (or NaN) at `loadedmetadata` — the webm header carries\n * no duration until the engine has scanned the stream. The standard workaround:\n * seek far past the end; the engine then computes the real duration and fires\n * `durationchange` with a finite value.\n *\n * Resolves the element's REAL finite duration in seconds, or `null` when it\n * cannot be determined within `timeoutMs` — callers decide their own fallback\n * (the file-input duration validator fails OPEN on null; the audio player\n * chrome keeps its placeholder). When the workaround seek actually ran,\n * `currentTime` is restored to 0 before resolving, so a paused just-mounted\n * preview is left visually untouched.\n */\nexport function resolveInfiniteDuration(\n el: HTMLMediaElement,\n timeoutMs = 3000,\n): Promise<number | null> {\n if (Number.isFinite(el.duration)) return Promise.resolve(el.duration);\n return new Promise((resolve) => {\n let done = false;\n const finish = (value: number | null) => {\n if (done) return;\n done = true;\n el.removeEventListener(\"durationchange\", onDurationChange);\n clearTimeout(timer);\n try {\n el.currentTime = 0; // undo the workaround seek\n } catch {\n // detached/errored element — the resolved value still stands\n }\n resolve(value);\n };\n const onDurationChange = () => {\n if (Number.isFinite(el.duration)) finish(el.duration);\n };\n const timer = setTimeout(() => finish(null), timeoutMs);\n el.addEventListener(\"durationchange\", onDurationChange);\n try {\n el.currentTime = Number.MAX_SAFE_INTEGER;\n } catch {\n finish(null);\n }\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"audio-player-chrome.d.ts","sourceRoot":"","sources":["../../src/react/audio-player-chrome.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AA8B5D,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnD,uEAAuE;IACvE,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAChC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,cAAuB,EACvB,UAAgC,EAChC,YAAY,GACb,EAAE,sBAAsB,qBA0RxB"}
1
+ {"version":3,"file":"audio-player-chrome.d.ts","sourceRoot":"","sources":["../../src/react/audio-player-chrome.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AA+B5D,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnD,uEAAuE;IACvE,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAChC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,cAAuB,EACvB,UAAgC,EAChC,YAAY,GACb,EAAE,sBAAsB,qBA0SxB"}
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  import * as React from "react";
16
16
  import { Pause, Play, Volume2, VolumeX, ChevronDown, ChevronUp, Activity, BarChart3 } from "lucide-react";
17
17
  import { startWaveformLoop, drawIdleFrame } from "../visualizer.js";
18
+ import { resolveInfiniteDuration } from "../media-duration.js";
18
19
  const DEFAULT_PERSIST_KEY = "mv-audio-player";
19
20
  function readPref(persistKey, suffix) {
20
21
  if (!persistKey)
@@ -131,6 +132,21 @@ export function AudioPlayerChrome({ audioRef, visualizerMode = "bars", persistKe
131
132
  const el = audioRef.current;
132
133
  if (!el)
133
134
  return;
135
+ // MediaRecorder blobs report duration Infinity until seeked (Chromium webm
136
+ // quirk) — without this the readout sticks at "0:01 / 0:00". Learn the real
137
+ // duration via the shared seek workaround; paused-only so an actively
138
+ // playing element is never seek-jumped, and one attempt per element so a
139
+ // workaround-triggered durationchange can never re-enter it.
140
+ let durationReconcileAttempted = false;
141
+ const reconcileInfiniteDuration = () => {
142
+ if (durationReconcileAttempted || Number.isFinite(el.duration) || !el.paused)
143
+ return;
144
+ durationReconcileAttempted = true;
145
+ void resolveInfiniteDuration(el).then((real) => {
146
+ if (real !== null)
147
+ setDuration(real);
148
+ });
149
+ };
134
150
  const syncFromElement = () => {
135
151
  setIsPlaying(!el.paused && !el.ended);
136
152
  setIsMuted(el.muted);
@@ -140,6 +156,8 @@ export function AudioPlayerChrome({ audioRef, visualizerMode = "bars", persistKe
140
156
  setCurrentTime(el.currentTime);
141
157
  };
142
158
  syncFromElement();
159
+ if (el.readyState >= 1)
160
+ reconcileInfiniteDuration();
143
161
  const onPlay = () => {
144
162
  setIsPlaying(true);
145
163
  ensureGraph(el);
@@ -157,6 +175,8 @@ export function AudioPlayerChrome({ audioRef, visualizerMode = "bars", persistKe
157
175
  const onDurationChange = () => {
158
176
  if (Number.isFinite(el.duration))
159
177
  setDuration(el.duration);
178
+ else
179
+ reconcileInfiniteDuration();
160
180
  };
161
181
  const onVolumeChange = () => {
162
182
  setIsMuted(el.muted);
@@ -1 +1 @@
1
- {"version":3,"file":"audio-player-chrome.js","sourceRoot":"","sources":["../../src/react/audio-player-chrome.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Custom audio player chrome — the one owned control surface used by every\n// AudioViewer. Owns: play/pause, seek, time readout, mute/volume, and\n// the visualizer panel (shared engine in ../visualizer.ts) with minimize and\n// line/bars mode toggles. The media ELEMENT (loading, errors, playback API)\n// stays owned by AudioViewer — this component only reads/drives it.\n//\n// Web Audio wiring is created lazily on the first `play` event (autoplay\n// policies suspend fresh AudioContexts; a user-gesture play resumes them) and\n// is best-effort throughout: if anything throws, the player still plays and\n// only the visualizer goes dark. `createMediaElementSource` is once-per-\n// element, so the graph is keyed to the element instance and rebuilt only if\n// the element itself changes.\n\nimport * as React from \"react\";\nimport { Pause, Play, Volume2, VolumeX, ChevronDown, ChevronUp, Activity, BarChart3 } from \"lucide-react\";\nimport { startWaveformLoop, drawIdleFrame } from \"../visualizer.js\";\nimport type { AudioVisualizerMode } from \"../visualizer.js\";\n\nconst DEFAULT_PERSIST_KEY = \"mv-audio-player\";\n\nfunction readPref(persistKey: string | null, suffix: string): string | null {\n if (!persistKey) return null;\n try {\n return window.localStorage.getItem(`${persistKey}:${suffix}`);\n } catch {\n return null;\n }\n}\n\nfunction writePref(persistKey: string | null, suffix: string, value: string): void {\n if (!persistKey) return;\n try {\n window.localStorage.setItem(`${persistKey}:${suffix}`, value);\n } catch {\n /* storage unavailable (private mode, SSR) — session-only prefs */\n }\n}\n\nfunction formatTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return \"0:00\";\n const whole = Math.floor(seconds);\n const m = Math.floor(whole / 60);\n const s = whole % 60;\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n}\n\nexport type AudioPlayerChromeProps = {\n audioRef: React.RefObject<HTMLAudioElement | null>;\n /** Initial visualizer mode; a persisted user choice wins over this. */\n visualizerMode?: AudioVisualizerMode;\n /** localStorage key base for minimize/mode prefs; `null` disables persistence. */\n persistKey?: string | null;\n /** App-injected action buttons, rendered at the end of the control row. */\n extraActions?: React.ReactNode;\n};\n\nexport function AudioPlayerChrome({\n audioRef,\n visualizerMode = \"bars\",\n persistKey = DEFAULT_PERSIST_KEY,\n extraActions,\n}: AudioPlayerChromeProps) {\n const [isPlaying, setIsPlaying] = React.useState(false);\n const [currentTime, setCurrentTime] = React.useState(0);\n const [duration, setDuration] = React.useState(0);\n const [isMuted, setIsMuted] = React.useState(false);\n const [volume, setVolume] = React.useState(1);\n const [minimized, setMinimized] = React.useState<boolean>(() => readPref(persistKey, \"minimized\") === \"1\");\n const [mode, setMode] = React.useState<AudioVisualizerMode>(() => {\n const stored = readPref(persistKey, \"mode\");\n return stored === \"line\" || stored === \"bars\" ? stored : visualizerMode;\n });\n\n // While the user drags the seek slider, timeupdate must not fight the thumb.\n const [scrubTime, setScrubTime] = React.useState<number | null>(null);\n\n const canvasRef = React.useRef<HTMLCanvasElement | null>(null);\n const modeRef = React.useRef(mode);\n modeRef.current = mode;\n\n // Web Audio graph (lazy, keyed to the element instance).\n const audioCtxRef = React.useRef<AudioContext | null>(null);\n const sourceElRef = React.useRef<HTMLAudioElement | null>(null);\n const analyserRef = React.useRef<AnalyserNode | null>(null);\n const stopLoopRef = React.useRef<(() => void) | null>(null);\n\n const stopLoop = React.useCallback(() => {\n stopLoopRef.current?.();\n stopLoopRef.current = null;\n }, []);\n\n const teardownGraph = React.useCallback(() => {\n stopLoop();\n analyserRef.current = null;\n sourceElRef.current = null;\n const ctx = audioCtxRef.current;\n audioCtxRef.current = null;\n if (ctx && ctx.state !== \"closed\") {\n ctx.close().catch(() => {});\n }\n }, [stopLoop]);\n\n const ensureGraph = React.useCallback(\n (el: HTMLAudioElement) => {\n try {\n if (sourceElRef.current === el && audioCtxRef.current) {\n if (audioCtxRef.current.state === \"suspended\") {\n audioCtxRef.current.resume().catch(() => {});\n }\n return;\n }\n teardownGraph();\n const AudioCtx: typeof AudioContext =\n (window as unknown as { AudioContext?: typeof AudioContext }).AudioContext ||\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext!;\n if (!AudioCtx) return;\n const ctx = new AudioCtx();\n const source = ctx.createMediaElementSource(el);\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n // Element audio now routes through the graph — destination connection\n // is REQUIRED or playback goes silent.\n source.connect(analyser);\n analyser.connect(ctx.destination);\n audioCtxRef.current = ctx;\n sourceElRef.current = el;\n analyserRef.current = analyser;\n if (ctx.state === \"suspended\") {\n ctx.resume().catch(() => {});\n }\n } catch {\n // Best-effort: playback works without the visualizer.\n analyserRef.current = null;\n }\n },\n [teardownGraph],\n );\n\n const startLoop = React.useCallback(() => {\n if (stopLoopRef.current) return;\n const analyser = analyserRef.current;\n if (!analyser) return;\n stopLoopRef.current = startWaveformLoop({\n analyser,\n getCanvas: () => canvasRef.current,\n mode: () => modeRef.current,\n });\n }, []);\n\n // Element event subscriptions. The element mounts in the same commit as this\n // chrome (AudioViewer renders both once loading starts), so the ref is set\n // by the time this effect runs.\n React.useEffect(() => {\n const el = audioRef.current;\n if (!el) return;\n\n const syncFromElement = () => {\n setIsPlaying(!el.paused && !el.ended);\n setIsMuted(el.muted);\n setVolume(el.volume);\n if (Number.isFinite(el.duration)) setDuration(el.duration);\n setCurrentTime(el.currentTime);\n };\n syncFromElement();\n\n const onPlay = () => {\n setIsPlaying(true);\n ensureGraph(el);\n startLoop();\n };\n const onPause = () => {\n setIsPlaying(false);\n stopLoop();\n };\n const onEnded = () => {\n setIsPlaying(false);\n stopLoop();\n };\n const onTimeUpdate = () => setCurrentTime(el.currentTime);\n const onDurationChange = () => {\n if (Number.isFinite(el.duration)) setDuration(el.duration);\n };\n const onVolumeChange = () => {\n setIsMuted(el.muted);\n setVolume(el.volume);\n };\n\n el.addEventListener(\"play\", onPlay);\n el.addEventListener(\"pause\", onPause);\n el.addEventListener(\"ended\", onEnded);\n el.addEventListener(\"timeupdate\", onTimeUpdate);\n el.addEventListener(\"durationchange\", onDurationChange);\n el.addEventListener(\"loadedmetadata\", onDurationChange);\n el.addEventListener(\"volumechange\", onVolumeChange);\n return () => {\n el.removeEventListener(\"play\", onPlay);\n el.removeEventListener(\"pause\", onPause);\n el.removeEventListener(\"ended\", onEnded);\n el.removeEventListener(\"timeupdate\", onTimeUpdate);\n el.removeEventListener(\"durationchange\", onDurationChange);\n el.removeEventListener(\"loadedmetadata\", onDurationChange);\n el.removeEventListener(\"volumechange\", onVolumeChange);\n };\n }, [audioRef, ensureGraph, startLoop, stopLoop]);\n\n // Full teardown when the chrome unmounts.\n React.useEffect(() => teardownGraph, [teardownGraph]);\n\n // Idle frame whenever the visualizer is visible but not animating.\n React.useEffect(() => {\n if (minimized || isPlaying) return;\n const canvas = canvasRef.current;\n if (canvas) drawIdleFrame(canvas, mode);\n }, [minimized, isPlaying, mode]);\n\n const togglePlay = () => {\n const el = audioRef.current;\n if (!el) return;\n if (el.paused) void el.play().catch(() => {});\n else el.pause();\n };\n\n const toggleMute = () => {\n const el = audioRef.current;\n if (!el) return;\n el.muted = !el.muted;\n };\n\n const onVolumeInput = (e: React.ChangeEvent<HTMLInputElement>) => {\n const el = audioRef.current;\n if (!el) return;\n const v = Number(e.target.value);\n el.volume = v;\n if (v > 0 && el.muted) el.muted = false;\n };\n\n const onSeekInput = (e: React.ChangeEvent<HTMLInputElement>) => {\n setScrubTime(Number(e.target.value));\n };\n\n const commitSeek = () => {\n const el = audioRef.current;\n if (el && scrubTime !== null) {\n try {\n el.currentTime = scrubTime;\n } catch {\n /* not seekable yet */\n }\n }\n setScrubTime(null);\n };\n\n const toggleMinimized = () => {\n setMinimized((m) => {\n writePref(persistKey, \"minimized\", m ? \"0\" : \"1\");\n return !m;\n });\n };\n\n const toggleMode = () => {\n setMode((prev) => {\n const next: AudioVisualizerMode = prev === \"line\" ? \"bars\" : \"line\";\n writePref(persistKey, \"mode\", next);\n return next;\n });\n };\n\n const shownTime = scrubTime ?? currentTime;\n\n return (\n <div className=\"mv-player\" data-mv-player data-minimized={minimized ? \"true\" : \"false\"} data-mode={mode}>\n {!minimized && (\n <div className=\"mv-player-viz\">\n <canvas ref={canvasRef} width={600} height={96} className=\"mv-player-canvas\" aria-hidden=\"true\" />\n <button\n type=\"button\"\n className=\"mv-player-btn mv-player-mode-btn\"\n onClick={toggleMode}\n aria-label={mode === \"line\" ? \"Switch visualizer to bars\" : \"Switch visualizer to waveform line\"}\n >\n {mode === \"line\" ? <BarChart3 aria-hidden=\"true\" /> : <Activity aria-hidden=\"true\" />}\n </button>\n </div>\n )}\n <div className=\"mv-player-controls\">\n <button\n type=\"button\"\n className=\"mv-player-btn mv-player-play-btn\"\n onClick={togglePlay}\n aria-label={isPlaying ? \"Pause\" : \"Play\"}\n >\n {isPlaying ? <Pause aria-hidden=\"true\" /> : <Play aria-hidden=\"true\" />}\n </button>\n <span className=\"mv-player-time\" aria-hidden=\"true\">\n {formatTime(shownTime)}\n </span>\n <input\n type=\"range\"\n className=\"mv-player-seek\"\n min={0}\n max={duration > 0 ? duration : 0}\n step={0.1}\n value={Math.min(shownTime, duration > 0 ? duration : shownTime)}\n onChange={onSeekInput}\n onMouseUp={commitSeek}\n onTouchEnd={commitSeek}\n onKeyUp={commitSeek}\n disabled={duration <= 0}\n aria-label=\"Seek\"\n />\n <span className=\"mv-player-time\" aria-hidden=\"true\">\n {formatTime(duration)}\n </span>\n <button\n type=\"button\"\n className=\"mv-player-btn\"\n onClick={toggleMute}\n aria-label={isMuted ? \"Unmute\" : \"Mute\"}\n >\n {isMuted ? <VolumeX aria-hidden=\"true\" /> : <Volume2 aria-hidden=\"true\" />}\n </button>\n <input\n type=\"range\"\n className=\"mv-player-volume\"\n min={0}\n max={1}\n step={0.05}\n value={isMuted ? 0 : volume}\n onChange={onVolumeInput}\n aria-label=\"Volume\"\n />\n <button\n type=\"button\"\n className=\"mv-player-btn\"\n onClick={toggleMinimized}\n aria-label={minimized ? \"Show visualizer\" : \"Hide visualizer\"}\n >\n {minimized ? <ChevronUp aria-hidden=\"true\" /> : <ChevronDown aria-hidden=\"true\" />}\n </button>\n {extraActions != null && <div className=\"mv-player-extra\">{extraActions}</div>}\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"audio-player-chrome.js","sourceRoot":"","sources":["../../src/react/audio-player-chrome.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Custom audio player chrome — the one owned control surface used by every\n// AudioViewer. Owns: play/pause, seek, time readout, mute/volume, and\n// the visualizer panel (shared engine in ../visualizer.ts) with minimize and\n// line/bars mode toggles. The media ELEMENT (loading, errors, playback API)\n// stays owned by AudioViewer — this component only reads/drives it.\n//\n// Web Audio wiring is created lazily on the first `play` event (autoplay\n// policies suspend fresh AudioContexts; a user-gesture play resumes them) and\n// is best-effort throughout: if anything throws, the player still plays and\n// only the visualizer goes dark. `createMediaElementSource` is once-per-\n// element, so the graph is keyed to the element instance and rebuilt only if\n// the element itself changes.\n\nimport * as React from \"react\";\nimport { Pause, Play, Volume2, VolumeX, ChevronDown, ChevronUp, Activity, BarChart3 } from \"lucide-react\";\nimport { startWaveformLoop, drawIdleFrame } from \"../visualizer.js\";\nimport type { AudioVisualizerMode } from \"../visualizer.js\";\nimport { resolveInfiniteDuration } from \"../media-duration.js\";\n\nconst DEFAULT_PERSIST_KEY = \"mv-audio-player\";\n\nfunction readPref(persistKey: string | null, suffix: string): string | null {\n if (!persistKey) return null;\n try {\n return window.localStorage.getItem(`${persistKey}:${suffix}`);\n } catch {\n return null;\n }\n}\n\nfunction writePref(persistKey: string | null, suffix: string, value: string): void {\n if (!persistKey) return;\n try {\n window.localStorage.setItem(`${persistKey}:${suffix}`, value);\n } catch {\n /* storage unavailable (private mode, SSR) — session-only prefs */\n }\n}\n\nfunction formatTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return \"0:00\";\n const whole = Math.floor(seconds);\n const m = Math.floor(whole / 60);\n const s = whole % 60;\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n}\n\nexport type AudioPlayerChromeProps = {\n audioRef: React.RefObject<HTMLAudioElement | null>;\n /** Initial visualizer mode; a persisted user choice wins over this. */\n visualizerMode?: AudioVisualizerMode;\n /** localStorage key base for minimize/mode prefs; `null` disables persistence. */\n persistKey?: string | null;\n /** App-injected action buttons, rendered at the end of the control row. */\n extraActions?: React.ReactNode;\n};\n\nexport function AudioPlayerChrome({\n audioRef,\n visualizerMode = \"bars\",\n persistKey = DEFAULT_PERSIST_KEY,\n extraActions,\n}: AudioPlayerChromeProps) {\n const [isPlaying, setIsPlaying] = React.useState(false);\n const [currentTime, setCurrentTime] = React.useState(0);\n const [duration, setDuration] = React.useState(0);\n const [isMuted, setIsMuted] = React.useState(false);\n const [volume, setVolume] = React.useState(1);\n const [minimized, setMinimized] = React.useState<boolean>(() => readPref(persistKey, \"minimized\") === \"1\");\n const [mode, setMode] = React.useState<AudioVisualizerMode>(() => {\n const stored = readPref(persistKey, \"mode\");\n return stored === \"line\" || stored === \"bars\" ? stored : visualizerMode;\n });\n\n // While the user drags the seek slider, timeupdate must not fight the thumb.\n const [scrubTime, setScrubTime] = React.useState<number | null>(null);\n\n const canvasRef = React.useRef<HTMLCanvasElement | null>(null);\n const modeRef = React.useRef(mode);\n modeRef.current = mode;\n\n // Web Audio graph (lazy, keyed to the element instance).\n const audioCtxRef = React.useRef<AudioContext | null>(null);\n const sourceElRef = React.useRef<HTMLAudioElement | null>(null);\n const analyserRef = React.useRef<AnalyserNode | null>(null);\n const stopLoopRef = React.useRef<(() => void) | null>(null);\n\n const stopLoop = React.useCallback(() => {\n stopLoopRef.current?.();\n stopLoopRef.current = null;\n }, []);\n\n const teardownGraph = React.useCallback(() => {\n stopLoop();\n analyserRef.current = null;\n sourceElRef.current = null;\n const ctx = audioCtxRef.current;\n audioCtxRef.current = null;\n if (ctx && ctx.state !== \"closed\") {\n ctx.close().catch(() => {});\n }\n }, [stopLoop]);\n\n const ensureGraph = React.useCallback(\n (el: HTMLAudioElement) => {\n try {\n if (sourceElRef.current === el && audioCtxRef.current) {\n if (audioCtxRef.current.state === \"suspended\") {\n audioCtxRef.current.resume().catch(() => {});\n }\n return;\n }\n teardownGraph();\n const AudioCtx: typeof AudioContext =\n (window as unknown as { AudioContext?: typeof AudioContext }).AudioContext ||\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext!;\n if (!AudioCtx) return;\n const ctx = new AudioCtx();\n const source = ctx.createMediaElementSource(el);\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n // Element audio now routes through the graph — destination connection\n // is REQUIRED or playback goes silent.\n source.connect(analyser);\n analyser.connect(ctx.destination);\n audioCtxRef.current = ctx;\n sourceElRef.current = el;\n analyserRef.current = analyser;\n if (ctx.state === \"suspended\") {\n ctx.resume().catch(() => {});\n }\n } catch {\n // Best-effort: playback works without the visualizer.\n analyserRef.current = null;\n }\n },\n [teardownGraph],\n );\n\n const startLoop = React.useCallback(() => {\n if (stopLoopRef.current) return;\n const analyser = analyserRef.current;\n if (!analyser) return;\n stopLoopRef.current = startWaveformLoop({\n analyser,\n getCanvas: () => canvasRef.current,\n mode: () => modeRef.current,\n });\n }, []);\n\n // Element event subscriptions. The element mounts in the same commit as this\n // chrome (AudioViewer renders both once loading starts), so the ref is set\n // by the time this effect runs.\n React.useEffect(() => {\n const el = audioRef.current;\n if (!el) return;\n\n // MediaRecorder blobs report duration Infinity until seeked (Chromium webm\n // quirk) — without this the readout sticks at \"0:01 / 0:00\". Learn the real\n // duration via the shared seek workaround; paused-only so an actively\n // playing element is never seek-jumped, and one attempt per element so a\n // workaround-triggered durationchange can never re-enter it.\n let durationReconcileAttempted = false;\n const reconcileInfiniteDuration = () => {\n if (durationReconcileAttempted || Number.isFinite(el.duration) || !el.paused) return;\n durationReconcileAttempted = true;\n void resolveInfiniteDuration(el).then((real) => {\n if (real !== null) setDuration(real);\n });\n };\n\n const syncFromElement = () => {\n setIsPlaying(!el.paused && !el.ended);\n setIsMuted(el.muted);\n setVolume(el.volume);\n if (Number.isFinite(el.duration)) setDuration(el.duration);\n setCurrentTime(el.currentTime);\n };\n syncFromElement();\n if (el.readyState >= 1) reconcileInfiniteDuration();\n\n const onPlay = () => {\n setIsPlaying(true);\n ensureGraph(el);\n startLoop();\n };\n const onPause = () => {\n setIsPlaying(false);\n stopLoop();\n };\n const onEnded = () => {\n setIsPlaying(false);\n stopLoop();\n };\n const onTimeUpdate = () => setCurrentTime(el.currentTime);\n const onDurationChange = () => {\n if (Number.isFinite(el.duration)) setDuration(el.duration);\n else reconcileInfiniteDuration();\n };\n const onVolumeChange = () => {\n setIsMuted(el.muted);\n setVolume(el.volume);\n };\n\n el.addEventListener(\"play\", onPlay);\n el.addEventListener(\"pause\", onPause);\n el.addEventListener(\"ended\", onEnded);\n el.addEventListener(\"timeupdate\", onTimeUpdate);\n el.addEventListener(\"durationchange\", onDurationChange);\n el.addEventListener(\"loadedmetadata\", onDurationChange);\n el.addEventListener(\"volumechange\", onVolumeChange);\n return () => {\n el.removeEventListener(\"play\", onPlay);\n el.removeEventListener(\"pause\", onPause);\n el.removeEventListener(\"ended\", onEnded);\n el.removeEventListener(\"timeupdate\", onTimeUpdate);\n el.removeEventListener(\"durationchange\", onDurationChange);\n el.removeEventListener(\"loadedmetadata\", onDurationChange);\n el.removeEventListener(\"volumechange\", onVolumeChange);\n };\n }, [audioRef, ensureGraph, startLoop, stopLoop]);\n\n // Full teardown when the chrome unmounts.\n React.useEffect(() => teardownGraph, [teardownGraph]);\n\n // Idle frame whenever the visualizer is visible but not animating.\n React.useEffect(() => {\n if (minimized || isPlaying) return;\n const canvas = canvasRef.current;\n if (canvas) drawIdleFrame(canvas, mode);\n }, [minimized, isPlaying, mode]);\n\n const togglePlay = () => {\n const el = audioRef.current;\n if (!el) return;\n if (el.paused) void el.play().catch(() => {});\n else el.pause();\n };\n\n const toggleMute = () => {\n const el = audioRef.current;\n if (!el) return;\n el.muted = !el.muted;\n };\n\n const onVolumeInput = (e: React.ChangeEvent<HTMLInputElement>) => {\n const el = audioRef.current;\n if (!el) return;\n const v = Number(e.target.value);\n el.volume = v;\n if (v > 0 && el.muted) el.muted = false;\n };\n\n const onSeekInput = (e: React.ChangeEvent<HTMLInputElement>) => {\n setScrubTime(Number(e.target.value));\n };\n\n const commitSeek = () => {\n const el = audioRef.current;\n if (el && scrubTime !== null) {\n try {\n el.currentTime = scrubTime;\n } catch {\n /* not seekable yet */\n }\n }\n setScrubTime(null);\n };\n\n const toggleMinimized = () => {\n setMinimized((m) => {\n writePref(persistKey, \"minimized\", m ? \"0\" : \"1\");\n return !m;\n });\n };\n\n const toggleMode = () => {\n setMode((prev) => {\n const next: AudioVisualizerMode = prev === \"line\" ? \"bars\" : \"line\";\n writePref(persistKey, \"mode\", next);\n return next;\n });\n };\n\n const shownTime = scrubTime ?? currentTime;\n\n return (\n <div className=\"mv-player\" data-mv-player data-minimized={minimized ? \"true\" : \"false\"} data-mode={mode}>\n {!minimized && (\n <div className=\"mv-player-viz\">\n <canvas ref={canvasRef} width={600} height={96} className=\"mv-player-canvas\" aria-hidden=\"true\" />\n <button\n type=\"button\"\n className=\"mv-player-btn mv-player-mode-btn\"\n onClick={toggleMode}\n aria-label={mode === \"line\" ? \"Switch visualizer to bars\" : \"Switch visualizer to waveform line\"}\n >\n {mode === \"line\" ? <BarChart3 aria-hidden=\"true\" /> : <Activity aria-hidden=\"true\" />}\n </button>\n </div>\n )}\n <div className=\"mv-player-controls\">\n <button\n type=\"button\"\n className=\"mv-player-btn mv-player-play-btn\"\n onClick={togglePlay}\n aria-label={isPlaying ? \"Pause\" : \"Play\"}\n >\n {isPlaying ? <Pause aria-hidden=\"true\" /> : <Play aria-hidden=\"true\" />}\n </button>\n <span className=\"mv-player-time\" aria-hidden=\"true\">\n {formatTime(shownTime)}\n </span>\n <input\n type=\"range\"\n className=\"mv-player-seek\"\n min={0}\n max={duration > 0 ? duration : 0}\n step={0.1}\n value={Math.min(shownTime, duration > 0 ? duration : shownTime)}\n onChange={onSeekInput}\n onMouseUp={commitSeek}\n onTouchEnd={commitSeek}\n onKeyUp={commitSeek}\n disabled={duration <= 0}\n aria-label=\"Seek\"\n />\n <span className=\"mv-player-time\" aria-hidden=\"true\">\n {formatTime(duration)}\n </span>\n <button\n type=\"button\"\n className=\"mv-player-btn\"\n onClick={toggleMute}\n aria-label={isMuted ? \"Unmute\" : \"Mute\"}\n >\n {isMuted ? <VolumeX aria-hidden=\"true\" /> : <Volume2 aria-hidden=\"true\" />}\n </button>\n <input\n type=\"range\"\n className=\"mv-player-volume\"\n min={0}\n max={1}\n step={0.05}\n value={isMuted ? 0 : volume}\n onChange={onVolumeInput}\n aria-label=\"Volume\"\n />\n <button\n type=\"button\"\n className=\"mv-player-btn\"\n onClick={toggleMinimized}\n aria-label={minimized ? \"Show visualizer\" : \"Hide visualizer\"}\n >\n {minimized ? <ChevronUp aria-hidden=\"true\" /> : <ChevronDown aria-hidden=\"true\" />}\n </button>\n {extraActions != null && <div className=\"mv-player-extra\">{extraActions}</div>}\n </div>\n </div>\n );\n}\n"]}
@@ -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
@@ -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
@@ -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 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"]}
@@ -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":";AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EACJ,GAAG,EACH,SAAS,EACT,cAAc,EACd,QAAQ,GAAG,IAAI,EACf,QAAQ,GAAG,KAAK,EAChB,KAAK,EACL,IAAI,GAAG,KAAK,EACZ,SAAS,EACT,gBAAgB,GAAG,IAAI,EACvB,IAAI,GAAG,IAAI,EACX,YAAY,GAAG,IAAI,EACnB,QAAQ,GAAG,KAAK,EAChB,QAAQ,EAAE,YAAY,GAAG,IAAI,EAC7B,OAAO,GAAG,UAAU,EACpB,iBAAiB,GAAG,KAAK,EACzB,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,EACb,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,mBAAmB,GACpB,GAAG,KAAK,CAAC;IAEV,MAAM,aAAa,GAAG,KAAK,IAAI,QAAQ,CAAC;IACxC,MAAM,cAAc,GAAG,iBAAiB,IAAI,QAAQ,CAAC;IAErD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5D,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAmB,IAAI,CAAC,CAAC;IAEtD,qEAAqE;IACrE,6EAA6E;IAC7E,8BAA8B;IAC9B,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAC/D,QAAQ,EACR,EAAE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,EACzD,mBAAmB,EACnB,GAAG,CACJ,CAAC;IAEF,qEAAqE;IACrE,yEAAyE;IACzE,6DAA6D;IAC7D,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;QAClD,WAAW,EAAE,CAAC,YAAY,IAAI,CAAC,cAAc;QAC7C,SAAS,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC;QACnB,UAAU,EAAE,OAAO;QACnB,IAAI,EAAE,CAAC,cAAc,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC;KAC7C,CAAC,CAAC;IAEH,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,iBAAiB,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC;IAE9D,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1B,aAAa,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,YAAY,IAAI,UAAU,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;YAC/D,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC;YAC/B,IAAI,KAAK,EAAE,CAAC;gBACV,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,KAAK,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;gBAC7B,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,CAAC;YACD,WAAW,CAAC,KAAK,CAAC,CAAC;YACnB,cAAc,CAAC,KAAK,CAAC,CAAC;YACtB,aAAa,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IAEvD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,cAAc,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEV,MAAM,gBAAgB,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QAC9C,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,MAAM,EAAE,EAAE,CAAC;IACb,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QACzC,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO,EAAE,EAAE,CAAC;IACd,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,oBAAoB,GAAG,KAAK,CAAC,WAAW,CAC5C,CAAC,CAAyC,EAAE,EAAE;QAC5C,cAAc,CAAC,IAAI,CAAC,CAAC;QACrB,gBAAgB,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,EACD,CAAC,gBAAgB,CAAC,CACnB,CAAC;IAEF,eAAe,CACb,UAAU,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW,IAAI,CAAC,QAAQ,EACpD,aAAa,EACb,WAAW,CACZ,CAAC;IAEF,4EAA4E;IAC5E,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,cAAc;YAAE,OAAO;QAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,KAAK,IAAI,CAAC,UAAU;YAAE,OAAO;QAClC,IAAI,cAAc,EAAE,CAAC;YACnB,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;aAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YACzB,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,EAAE,CAAC,cAAc,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC;IAEjD,aAAa;IACb,MAAM,iBAAiB,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QAC/C,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAC9B,MAAM,EAAE,GAAG,QAAQ,CAAC,OAInB,CAAC;QACF,IAAI,CAAC,EAAE;YAAE,OAAO;QAChB,MAAM,EAAE,GACN,EAAE,CAAC,iBAAiB;YACpB,EAAE,CAAC,uBAAuB;YAC1B,EAAE,CAAC,oBAAoB;YACvB,EAAE,CAAC,mBAAmB,CAAC;QACzB,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;YAC7B,IAAI,CAAC;gBAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAEvB,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CACjC,CAAC,CAAsB,EAAE,EAAE;QACzB,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAC9B,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;YACvC,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,iBAAiB,EAAE,CAAC;QACtB,CAAC;IACH,CAAC,EACD,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CACtC,CAAC;IAEF,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,QAAQ;YAAE,OAAO,4BAAG,QAAQ,GAAI,CAAC;QACrC,OAAO,CACL,cAAK,SAAS,EAAE,SAAS,YACvB,cAAK,SAAS,EAAC,gBAAgB,YAC7B,+CAA2B,GACvB,GACF,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,eACE,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAC7C,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,gBAC9B,gBAAgB,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,SAAS,EAClE,aAAa,EAAE,iBAAiB,EAChC,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,aAElG,YAAY,IAAI,CAAC,QAAQ,IAAI,UAAU,IAAI,CAC1C,KAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAI,CACvF,EACA,UAAU,IAAI,CACb,gBACE,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,gBAAgB,EAC9B,OAAO,EAAE,WAAW,EACpB,gBAAgB,EAAE,oBAAoB,EACtC,YAAY,EAAE,gBAAgB,CAAC,YAAY,EAC3C,MAAM,EAAE,gBAAgB,CAAC,MAAM,EAC/B,OAAO,EAAE,gBAAgB,CAAC,OAAO,EACjC,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,EACnC,OAAO,EAAE,gBAAgB,CAAC,OAAO,EACjC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EACpC,WAAW,QACX,KAAK,EAAE;oBACL,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM;oBACd,SAAS,EAAE,OAAO;oBAClB,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;oBACzB,UAAU,EAAE,oBAAoB;oBAChC,gBAAgB,EAAE,MAAM;oBACxB,kBAAkB,EAAE,MAAM;oBAC1B,uBAAuB,EAAE,aAAa;iBAChB,GACxB,CACH,EACA,UAAU,IAAI,IAAI,IAAI,QAAQ,IAAI,CACjC,cACE,SAAS,EAAC,gBAAgB,EAC1B,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,YAEnD,UAAU,GACP,CACP,IACG,CACP,CAAC;AACJ,CAAC","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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/media-viewer",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
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.7"
36
+ "@ttt-productions/media-schemas": "^0.14.3",
37
+ "@ttt-productions/ui-core": "^0.11.10"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": ">=19.0.0",