@lucaismyname/ginger 0.0.31 → 0.0.32

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.
Files changed (62) hide show
  1. package/dist/client.cjs +1 -1
  2. package/dist/client.js +37 -36
  3. package/dist/client.js.map +1 -1
  4. package/dist/equalizer/index.cjs +1 -1
  5. package/dist/equalizer/index.cjs.map +1 -1
  6. package/dist/equalizer/index.js +16 -15
  7. package/dist/equalizer/index.js.map +1 -1
  8. package/dist/index.cjs +1 -1
  9. package/dist/index.js +37 -36
  10. package/dist/index.js.map +1 -1
  11. package/dist/liveAudioGraph-0cpHD_Ic.cjs +2 -0
  12. package/dist/liveAudioGraph-0cpHD_Ic.cjs.map +1 -0
  13. package/dist/liveAudioGraph-DvPaxBCP.js +105 -0
  14. package/dist/liveAudioGraph-DvPaxBCP.js.map +1 -0
  15. package/dist/remote/index.cjs +2 -0
  16. package/dist/remote/index.cjs.map +1 -0
  17. package/dist/remote/index.d.ts +5 -0
  18. package/dist/remote/index.d.ts.map +1 -0
  19. package/dist/remote/index.js +149 -0
  20. package/dist/remote/index.js.map +1 -0
  21. package/dist/remote/remoteProtocol.d.ts +28 -0
  22. package/dist/remote/remoteProtocol.d.ts.map +1 -0
  23. package/dist/remote/useGingerRemote.d.ts +35 -0
  24. package/dist/remote/useGingerRemote.d.ts.map +1 -0
  25. package/dist/spatial/index.cjs +2 -0
  26. package/dist/spatial/index.cjs.map +1 -0
  27. package/dist/spatial/index.d.ts +3 -0
  28. package/dist/spatial/index.d.ts.map +1 -0
  29. package/dist/spatial/index.js +59 -0
  30. package/dist/spatial/index.js.map +1 -0
  31. package/dist/spatial/useGingerSpatialAudio.d.ts +34 -0
  32. package/dist/spatial/useGingerSpatialAudio.d.ts.map +1 -0
  33. package/dist/spatial/useGingerSpatialAudio.test.d.ts +2 -0
  34. package/dist/spatial/useGingerSpatialAudio.test.d.ts.map +1 -0
  35. package/dist/testing/mockWebAudio.d.ts +14 -0
  36. package/dist/testing/mockWebAudio.d.ts.map +1 -1
  37. package/dist/transcript/index.cjs +8 -0
  38. package/dist/transcript/index.cjs.map +1 -0
  39. package/dist/transcript/index.d.ts +5 -0
  40. package/dist/transcript/index.d.ts.map +1 -0
  41. package/dist/transcript/index.js +99 -0
  42. package/dist/transcript/index.js.map +1 -0
  43. package/dist/transcript/parseTranscript.d.ts +27 -0
  44. package/dist/transcript/parseTranscript.d.ts.map +1 -0
  45. package/dist/transcript/parseTranscript.test.d.ts +2 -0
  46. package/dist/transcript/parseTranscript.test.d.ts.map +1 -0
  47. package/dist/transcript/useGingerTranscriptSync.d.ts +23 -0
  48. package/dist/transcript/useGingerTranscriptSync.d.ts.map +1 -0
  49. package/dist/useGinger-BXgia32v.cjs +2 -0
  50. package/dist/useGinger-BXgia32v.cjs.map +1 -0
  51. package/dist/useGinger-hpp2pAGY.js +48 -0
  52. package/dist/useGinger-hpp2pAGY.js.map +1 -0
  53. package/dist/useGingerChapterProgress-BdaalJvX.cjs +2 -0
  54. package/dist/{useGingerChapterProgress-BOqUimE7.cjs.map → useGingerChapterProgress-BdaalJvX.cjs.map} +1 -1
  55. package/dist/{useGingerChapterProgress-DLYdGytK.js → useGingerChapterProgress-CZdv-HiI.js} +23 -22
  56. package/dist/{useGingerChapterProgress-DLYdGytK.js.map → useGingerChapterProgress-CZdv-HiI.js.map} +1 -1
  57. package/package.json +17 -2
  58. package/dist/liveAudioGraph-CmEsdLgZ.js +0 -150
  59. package/dist/liveAudioGraph-CmEsdLgZ.js.map +0 -1
  60. package/dist/liveAudioGraph-D1BXMv_u.cjs +0 -2
  61. package/dist/liveAudioGraph-D1BXMv_u.cjs.map +0 -1
  62. package/dist/useGingerChapterProgress-BOqUimE7.cjs +0 -2
@@ -0,0 +1,149 @@
1
+ import { useRef as s, useState as k, useCallback as N, useEffect as _ } from "react";
2
+ import { u as x } from "../useGinger-hpp2pAGY.js";
3
+ const F = "ginger-remote";
4
+ function z() {
5
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `ginger-tab-${Math.random().toString(36).slice(2)}`;
6
+ }
7
+ function $(G = {}) {
8
+ const {
9
+ channelName: h = F,
10
+ heartbeatMs: p = 2e3,
11
+ electionTimeoutMs: T = 300
12
+ } = G, { state: r, init: v } = x(), e = s("");
13
+ e.current === "" && (e.current = z());
14
+ const [l, i] = k("pending"), [H, g] = k(0), [O, L] = k(null), t = s(l);
15
+ t.current = l;
16
+ const M = s(v);
17
+ M.current = v;
18
+ const A = s(null), o = s(/* @__PURE__ */ new Set()), I = s(Date.now()), E = s(null), u = s(null), R = s(null), m = N((c) => {
19
+ var a;
20
+ (a = A.current) == null || a.postMessage(c);
21
+ }, []), f = N(() => {
22
+ E.current && (clearTimeout(E.current), E.current = null);
23
+ }, []), y = N(() => {
24
+ u.current && (clearInterval(u.current), u.current = null);
25
+ }, []);
26
+ _(() => {
27
+ if (typeof window > "u" || typeof BroadcastChannel > "u") {
28
+ L("BroadcastChannel is not available in this environment");
29
+ return;
30
+ }
31
+ L(null);
32
+ const c = new BroadcastChannel(h);
33
+ A.current = c, o.current = /* @__PURE__ */ new Set([e.current]);
34
+ const a = (b) => {
35
+ c.postMessage(b);
36
+ }, w = () => {
37
+ u.current && clearInterval(u.current), u.current = setInterval(() => {
38
+ const b = e.current;
39
+ o.current.add(b);
40
+ const n = o.current.size;
41
+ g(n), a({ type: "HEARTBEAT", tabId: b, connectedCount: n });
42
+ }, p);
43
+ }, D = () => {
44
+ f(), y(), i("follower"), t.current = "follower";
45
+ }, B = () => {
46
+ f(), i("leader"), t.current = "leader", o.current.add(e.current), a({ type: "LEADER_ANNOUNCE", tabId: e.current }), w();
47
+ }, C = () => {
48
+ f(), E.current = setTimeout(() => {
49
+ t.current === "pending" && (i("leader"), t.current = "leader", o.current.add(e.current), a({ type: "LEADER_ANNOUNCE", tabId: e.current }), w());
50
+ }, T);
51
+ }, S = (b) => {
52
+ const n = b.data;
53
+ if (!n || typeof n != "object" || !("type" in n)) return;
54
+ const d = e.current;
55
+ switch (n.type) {
56
+ case "PING": {
57
+ o.current.add(n.tabId), t.current === "leader" && a({ type: "PONG", tabId: d, leaderTabId: d });
58
+ break;
59
+ }
60
+ case "PONG": {
61
+ n.leaderTabId && n.leaderTabId !== d && (D(), I.current = Date.now());
62
+ break;
63
+ }
64
+ case "LEADER_ANNOUNCE": {
65
+ if (n.tabId === d) break;
66
+ o.current.add(n.tabId), n.tabId < d ? (D(), I.current = Date.now()) : n.tabId > d && (t.current === "pending" || t.current === "leader") && B();
67
+ break;
68
+ }
69
+ case "LEADER_RESIGN": {
70
+ if (n.tabId === d) break;
71
+ I.current = Date.now(), t.current === "follower" && (i("pending"), t.current = "pending", a({ type: "PING", tabId: d }), C());
72
+ break;
73
+ }
74
+ case "HEARTBEAT": {
75
+ t.current === "follower" && (I.current = Date.now(), g(n.connectedCount));
76
+ break;
77
+ }
78
+ case "STATE_SNAPSHOT": {
79
+ t.current === "follower" && n.tabId !== d && M.current(n.snapshot);
80
+ break;
81
+ }
82
+ }
83
+ };
84
+ c.addEventListener("message", S), a({ type: "PING", tabId: e.current }), E.current = setTimeout(() => {
85
+ t.current === "pending" && (i("leader"), t.current = "leader", o.current.add(e.current), a({ type: "LEADER_ANNOUNCE", tabId: e.current }), w());
86
+ }, T), R.current = setInterval(() => {
87
+ t.current === "follower" && Date.now() - I.current > p * 2 && (i("pending"), t.current = "pending", a({ type: "PING", tabId: e.current }), C());
88
+ }, p);
89
+ const P = () => {
90
+ t.current === "leader" && a({ type: "LEADER_RESIGN", tabId: e.current });
91
+ };
92
+ return window.addEventListener("pagehide", P), () => {
93
+ window.removeEventListener("pagehide", P), f(), y(), R.current && (clearInterval(R.current), R.current = null), c.removeEventListener("message", S), t.current === "leader" && a({ type: "LEADER_RESIGN", tabId: e.current }), c.close(), A.current = null;
94
+ };
95
+ }, [h, f, T, p, y]), _(() => {
96
+ if (l !== "leader") return;
97
+ const c = {
98
+ tracks: r.tracks,
99
+ currentIndex: r.currentIndex,
100
+ playlistMeta: r.playlistMeta,
101
+ isPaused: r.isPaused,
102
+ /** Avoid `createInitialState` re-shuffling on followers; queue order is already canonical. */
103
+ isShuffled: !1,
104
+ repeatMode: r.repeatMode,
105
+ playbackMode: r.playbackMode,
106
+ volume: r.volume,
107
+ muted: r.muted,
108
+ playbackRate: r.playbackRate
109
+ };
110
+ m({
111
+ type: "STATE_SNAPSHOT",
112
+ tabId: e.current,
113
+ snapshot: c
114
+ });
115
+ }, [
116
+ l,
117
+ r.tracks,
118
+ r.currentIndex,
119
+ r.isPaused,
120
+ r.repeatMode,
121
+ r.playbackMode,
122
+ r.playlistMeta,
123
+ r.volume,
124
+ r.muted,
125
+ r.playbackRate,
126
+ m
127
+ ]);
128
+ const U = N(() => {
129
+ f(), y(), i("leader"), t.current = "leader", o.current.add(e.current), m({ type: "LEADER_ANNOUNCE", tabId: e.current }), u.current && clearInterval(u.current), u.current = setInterval(() => {
130
+ const c = e.current;
131
+ o.current.add(c);
132
+ const a = o.current.size;
133
+ g(a), m({ type: "HEARTBEAT", tabId: c, connectedCount: a });
134
+ }, p);
135
+ }, [f, p, m, y]);
136
+ return {
137
+ isLeader: l === "leader",
138
+ isFollower: l === "follower",
139
+ isPending: l === "pending",
140
+ connectedTabs: H,
141
+ claimLeadership: U,
142
+ error: O
143
+ };
144
+ }
145
+ export {
146
+ F as DEFAULT_REMOTE_CHANNEL_NAME,
147
+ $ as useGingerRemote
148
+ };
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/remote/remoteProtocol.ts","../../src/remote/useGingerRemote.ts"],"sourcesContent":["import type { GingerInitPayload } from \"../types\";\n\nexport const DEFAULT_REMOTE_CHANNEL_NAME = \"ginger-remote\";\n\n/**\n * Cross-tab messages for {@link useGingerRemote}.\n */\nexport type RemoteMessage =\n | { type: \"PING\"; tabId: string }\n | { type: \"PONG\"; tabId: string; leaderTabId: string }\n | { type: \"LEADER_ANNOUNCE\"; tabId: string }\n | { type: \"LEADER_RESIGN\"; tabId: string }\n | { type: \"HEARTBEAT\"; tabId: string; connectedCount: number }\n | { type: \"STATE_SNAPSHOT\"; tabId: string; snapshot: GingerInitPayload };\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport type { GingerInitPayload } from \"../types\";\nimport { DEFAULT_REMOTE_CHANNEL_NAME, type RemoteMessage } from \"./remoteProtocol\";\n\nexport type UseGingerRemoteOptions = {\n /** BroadcastChannel name. Default: `\"ginger-remote\"`. */\n channelName?: string;\n /** Leader heartbeat interval in ms. Default: `2000`. */\n heartbeatMs?: number;\n /** Time to wait for an existing leader before claiming leadership. Default: `300`. */\n electionTimeoutMs?: number;\n};\n\nexport type UseGingerRemoteResult = {\n isLeader: boolean;\n isFollower: boolean;\n /** True until a leader is elected or this tab becomes leader. */\n isPending: boolean;\n connectedTabs: number;\n /** Request leadership (other tabs may win if their `tabId` is lexicographically smaller). */\n claimLeadership: () => void;\n error: string | null;\n};\n\nfunction makeTabId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `ginger-tab-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Multi-tab coordination via `BroadcastChannel`: elects a single leader tab and syncs\n * playback state to followers with `INIT` snapshots.\n *\n * Mount `Ginger.Player` only on the leader tab so one `<audio>` element plays:\n *\n * ```tsx\n * const { isLeader } = useGingerRemote();\n * return <>{isLeader && <Ginger.Player />}</>;\n * ```\n *\n * ```ts\n * import { useGingerRemote } from \"@lucaismyname/ginger/remote\";\n * ```\n */\nexport function useGingerRemote(options: UseGingerRemoteOptions = {}): UseGingerRemoteResult {\n const {\n channelName = DEFAULT_REMOTE_CHANNEL_NAME,\n heartbeatMs = 2000,\n electionTimeoutMs = 300,\n } = options;\n\n const { state, init } = useGinger();\n\n const tabIdRef = useRef<string>(\"\");\n if (tabIdRef.current === \"\") {\n tabIdRef.current = makeTabId();\n }\n\n const [role, setRole] = useState<\"pending\" | \"leader\" | \"follower\">(\"pending\");\n const [connectedTabs, setConnectedTabs] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const roleRef = useRef(role);\n roleRef.current = role;\n\n const initRef = useRef(init);\n initRef.current = init;\n\n const channelRef = useRef<BroadcastChannel | null>(null);\n const knownTabsRef = useRef<Set<string>>(new Set());\n const lastHeartbeatAtRef = useRef<number>(Date.now());\n const electionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const heartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const leaderWatchRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const post = useCallback((msg: RemoteMessage) => {\n channelRef.current?.postMessage(msg);\n }, []);\n\n const clearElectionTimer = useCallback(() => {\n if (electionTimerRef.current) {\n clearTimeout(electionTimerRef.current);\n electionTimerRef.current = null;\n }\n }, []);\n\n const stopHeartbeat = useCallback(() => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n heartbeatTimerRef.current = null;\n }\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n setError(\"BroadcastChannel is not available in this environment\");\n return;\n }\n\n setError(null);\n const ch = new BroadcastChannel(channelName);\n channelRef.current = ch;\n knownTabsRef.current = new Set([tabIdRef.current]);\n\n const postMsg = (msg: RemoteMessage) => {\n ch.postMessage(msg);\n };\n\n const startLeaderHeartbeat = () => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n postMsg({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n };\n\n const becomeFollower = () => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"follower\");\n roleRef.current = \"follower\";\n };\n\n const becomeLeaderFromRemote = () => {\n clearElectionTimer();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n };\n\n const scheduleElection = () => {\n clearElectionTimer();\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n };\n\n const onMessage = (ev: MessageEvent<RemoteMessage>) => {\n const msg = ev.data;\n if (!msg || typeof msg !== \"object\" || !(\"type\" in msg)) return;\n\n const myId = tabIdRef.current;\n\n switch (msg.type) {\n case \"PING\": {\n knownTabsRef.current.add(msg.tabId);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"PONG\", tabId: myId, leaderTabId: myId });\n }\n break;\n }\n case \"PONG\": {\n if (msg.leaderTabId && msg.leaderTabId !== myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n }\n break;\n }\n case \"LEADER_ANNOUNCE\": {\n if (msg.tabId === myId) break;\n knownTabsRef.current.add(msg.tabId);\n if (msg.tabId < myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n } else if (\n msg.tabId > myId &&\n (roleRef.current === \"pending\" || roleRef.current === \"leader\")\n ) {\n becomeLeaderFromRemote();\n }\n break;\n }\n case \"LEADER_RESIGN\": {\n if (msg.tabId === myId) break;\n lastHeartbeatAtRef.current = Date.now();\n if (roleRef.current === \"follower\") {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: myId });\n scheduleElection();\n }\n break;\n }\n case \"HEARTBEAT\": {\n if (roleRef.current === \"follower\") {\n lastHeartbeatAtRef.current = Date.now();\n setConnectedTabs(msg.connectedCount);\n }\n break;\n }\n case \"STATE_SNAPSHOT\": {\n if (roleRef.current === \"follower\" && msg.tabId !== myId) {\n initRef.current(msg.snapshot);\n }\n break;\n }\n default:\n break;\n }\n };\n\n ch.addEventListener(\"message\", onMessage);\n\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n\n leaderWatchRef.current = setInterval(() => {\n if (roleRef.current !== \"follower\") return;\n if (Date.now() - lastHeartbeatAtRef.current > heartbeatMs * 2) {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n scheduleElection();\n }\n }, heartbeatMs);\n\n const onPageHide = () => {\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n };\n window.addEventListener(\"pagehide\", onPageHide);\n\n return () => {\n window.removeEventListener(\"pagehide\", onPageHide);\n clearElectionTimer();\n stopHeartbeat();\n if (leaderWatchRef.current) {\n clearInterval(leaderWatchRef.current);\n leaderWatchRef.current = null;\n }\n ch.removeEventListener(\"message\", onMessage);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n ch.close();\n channelRef.current = null;\n };\n }, [channelName, clearElectionTimer, electionTimeoutMs, heartbeatMs, stopHeartbeat]);\n\n useEffect(() => {\n if (role !== \"leader\") return;\n const snapshot: GingerInitPayload = {\n tracks: state.tracks,\n currentIndex: state.currentIndex,\n playlistMeta: state.playlistMeta,\n isPaused: state.isPaused,\n /** Avoid `createInitialState` re-shuffling on followers; queue order is already canonical. */\n isShuffled: false,\n repeatMode: state.repeatMode,\n playbackMode: state.playbackMode,\n volume: state.volume,\n muted: state.muted,\n playbackRate: state.playbackRate,\n };\n post({\n type: \"STATE_SNAPSHOT\",\n tabId: tabIdRef.current,\n snapshot,\n });\n }, [\n role,\n state.tracks,\n state.currentIndex,\n state.isPaused,\n state.repeatMode,\n state.playbackMode,\n state.playlistMeta,\n state.volume,\n state.muted,\n state.playbackRate,\n post,\n ]);\n\n const claimLeadership = useCallback(() => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n post({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n post({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n }, [clearElectionTimer, heartbeatMs, post, stopHeartbeat]);\n\n return {\n isLeader: role === \"leader\",\n isFollower: role === \"follower\",\n isPending: role === \"pending\",\n connectedTabs,\n claimLeadership,\n error,\n };\n}\n"],"names":["DEFAULT_REMOTE_CHANNEL_NAME","makeTabId","useGingerRemote","options","channelName","heartbeatMs","electionTimeoutMs","state","init","useGinger","tabIdRef","useRef","role","setRole","useState","connectedTabs","setConnectedTabs","error","setError","roleRef","initRef","channelRef","knownTabsRef","lastHeartbeatAtRef","electionTimerRef","heartbeatTimerRef","leaderWatchRef","post","useCallback","msg","_a","clearElectionTimer","stopHeartbeat","useEffect","ch","postMsg","startLeaderHeartbeat","leaderId","count","becomeFollower","becomeLeaderFromRemote","scheduleElection","onMessage","ev","myId","onPageHide","snapshot","claimLeadership"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { GingerInitPayload } from '../types';
2
+ export declare const DEFAULT_REMOTE_CHANNEL_NAME = "ginger-remote";
3
+ /**
4
+ * Cross-tab messages for {@link useGingerRemote}.
5
+ */
6
+ export type RemoteMessage = {
7
+ type: "PING";
8
+ tabId: string;
9
+ } | {
10
+ type: "PONG";
11
+ tabId: string;
12
+ leaderTabId: string;
13
+ } | {
14
+ type: "LEADER_ANNOUNCE";
15
+ tabId: string;
16
+ } | {
17
+ type: "LEADER_RESIGN";
18
+ tabId: string;
19
+ } | {
20
+ type: "HEARTBEAT";
21
+ tabId: string;
22
+ connectedCount: number;
23
+ } | {
24
+ type: "STATE_SNAPSHOT";
25
+ tabId: string;
26
+ snapshot: GingerInitPayload;
27
+ };
28
+ //# sourceMappingURL=remoteProtocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remoteProtocol.d.ts","sourceRoot":"","sources":["../../src/remote/remoteProtocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,eAAO,MAAM,2BAA2B,kBAAkB,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAAC"}
@@ -0,0 +1,35 @@
1
+ export type UseGingerRemoteOptions = {
2
+ /** BroadcastChannel name. Default: `"ginger-remote"`. */
3
+ channelName?: string;
4
+ /** Leader heartbeat interval in ms. Default: `2000`. */
5
+ heartbeatMs?: number;
6
+ /** Time to wait for an existing leader before claiming leadership. Default: `300`. */
7
+ electionTimeoutMs?: number;
8
+ };
9
+ export type UseGingerRemoteResult = {
10
+ isLeader: boolean;
11
+ isFollower: boolean;
12
+ /** True until a leader is elected or this tab becomes leader. */
13
+ isPending: boolean;
14
+ connectedTabs: number;
15
+ /** Request leadership (other tabs may win if their `tabId` is lexicographically smaller). */
16
+ claimLeadership: () => void;
17
+ error: string | null;
18
+ };
19
+ /**
20
+ * Multi-tab coordination via `BroadcastChannel`: elects a single leader tab and syncs
21
+ * playback state to followers with `INIT` snapshots.
22
+ *
23
+ * Mount `Ginger.Player` only on the leader tab so one `<audio>` element plays:
24
+ *
25
+ * ```tsx
26
+ * const { isLeader } = useGingerRemote();
27
+ * return <>{isLeader && <Ginger.Player />}</>;
28
+ * ```
29
+ *
30
+ * ```ts
31
+ * import { useGingerRemote } from "@lucaismyname/ginger/remote";
32
+ * ```
33
+ */
34
+ export declare function useGingerRemote(options?: UseGingerRemoteOptions): UseGingerRemoteResult;
35
+ //# sourceMappingURL=useGingerRemote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerRemote.d.ts","sourceRoot":"","sources":["../../src/remote/useGingerRemote.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,sBAAsB,GAAG;IACnC,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,iEAAiE;IACjE,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AASF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,qBAAqB,CAuR3F"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("react"),d=require("../liveAudioGraph-0cpHD_Ic.cjs"),k=require("../useGinger-BXgia32v.cjs");function M(t,o,c,l){t.positionX.value=o,t.positionY.value=c,t.positionZ.value=l}function R(t,o,c,l){t.listener.positionX.value=o,t.listener.positionY.value=c,t.listener.positionZ.value=l}function q(t={}){const{enabled:o=!0,panningModel:c="HRTF",distanceModel:l="inverse",refDistance:g=1,position:S=[0,0,0],listenerPosition:A=[0,0,0]}=t,{audioRef:p}=k.useGinger(),[G,v]=r.useState(null),u=r.useRef(null),f=r.useRef(null),[P,h,m]=S,[b,y,C]=A;r.useEffect(()=>{const n=p.current;if(!(!n||typeof window>"u")){if(!o){d.setProcessingChain(n,[]),u.current=null,f.current=null;return}try{const e=d.attachLiveAnalyser(n,{fftSize:32,smoothingTimeConstant:0,minDecibels:-100,maxDecibels:0}),{context:s,id:i}=e,a=s.createPanner();a.panningModel=c,a.distanceModel=l,a.refDistance=g,M(a,P,h,m),R(s,b,y,C),u.current=a,f.current=s,d.setProcessingChain(n,[a]),d.detachLiveAnalyser(n,i),v(null)}catch(e){const s=e instanceof Error?e.message:"Failed to create spatial panner";v(s),u.current=null,f.current=null}return()=>{const e=p.current;e&&d.setProcessingChain(e,[]),u.current=null,f.current=null}}},[o,c,l,g,P,h,m,b,y,C,p]);const x=r.useCallback((n,e,s)=>{const i=u.current;i&&M(i,n,e,s)},[]),D=r.useCallback((n,e,s)=>{const i=f.current;i&&R(i,n,e,s)},[]),L=r.useCallback(n=>{const e=u.current;e&&(e.panningModel=n)},[]);return{setSourcePosition:x,setListenerPosition:D,setPanningModel:L,error:G}}exports.useGingerSpatialAudio=q;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../../src/spatial/useGingerSpatialAudio.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n attachLiveAnalyser,\n detachLiveAnalyser,\n setProcessingChain,\n} from \"../analyzer/liveAudioGraph\";\nimport { useGinger } from \"../hooks/useGinger\";\n\n/** `[x, y, z]` in Web Audio space (meters). */\nexport type SpatialPosition = [number, number, number];\n\nexport type UseGingerSpatialAudioOptions = {\n /** When false, the panner is removed and the audio path is bypassed. Default: true. */\n enabled?: boolean;\n /** Default: `\"HRTF\"`. */\n panningModel?: PanningModelType;\n /** Default: `\"inverse\"`. */\n distanceModel?: DistanceModelType;\n /** Reference distance for attenuation. Default: `1`. */\n refDistance?: number;\n /** Source position. Default: `[0, 0, 0]`. */\n position?: SpatialPosition;\n /** Listener position. Default: `[0, 0, 0]`. */\n listenerPosition?: SpatialPosition;\n};\n\nexport type UseGingerSpatialAudioResult = {\n setSourcePosition: (x: number, y: number, z: number) => void;\n setListenerPosition: (x: number, y: number, z: number) => void;\n setPanningModel: (model: PanningModelType) => void;\n error: string | null;\n};\n\nfunction setPannerPosition(panner: PannerNode, x: number, y: number, z: number): void {\n panner.positionX.value = x;\n panner.positionY.value = y;\n panner.positionZ.value = z;\n}\n\nfunction setListenerXYZ(context: AudioContext, x: number, y: number, z: number): void {\n context.listener.positionX.value = x;\n context.listener.positionY.value = y;\n context.listener.positionZ.value = z;\n}\n\n/**\n * Inserts an HRTF `PannerNode` into the Web Audio graph for the active Ginger media element.\n *\n * Shares the same `AudioContext` as `useGingerEqualizer` / `useGingerLiveAnalyzer` — only one\n * `MediaElementAudioSourceNode` per element is allowed by the browser.\n *\n * ```ts\n * import { useGingerSpatialAudio } from \"@lucaismyname/ginger/spatial\";\n * ```\n */\nexport function useGingerSpatialAudio(\n options: UseGingerSpatialAudioOptions = {},\n): UseGingerSpatialAudioResult {\n const {\n enabled = true,\n panningModel = \"HRTF\",\n distanceModel = \"inverse\",\n refDistance = 1,\n position = [0, 0, 0],\n listenerPosition = [0, 0, 0],\n } = options;\n const { audioRef } = useGinger();\n const [error, setError] = useState<string | null>(null);\n\n const pannerRef = useRef<PannerNode | null>(null);\n const contextRef = useRef<AudioContext | null>(null);\n\n const [sx, sy, sz] = position;\n const [lx, ly, lz] = listenerPosition;\n\n useEffect(() => {\n const el = audioRef.current;\n if (!el || typeof window === \"undefined\") {\n return;\n }\n\n if (!enabled) {\n setProcessingChain(el, []);\n pannerRef.current = null;\n contextRef.current = null;\n return;\n }\n\n try {\n const attached = attachLiveAnalyser(el, {\n fftSize: 32,\n smoothingTimeConstant: 0,\n minDecibels: -100,\n maxDecibels: 0,\n });\n const { context, id: tempId } = attached;\n\n const panner = context.createPanner();\n panner.panningModel = panningModel;\n panner.distanceModel = distanceModel;\n panner.refDistance = refDistance;\n setPannerPosition(panner, sx, sy, sz);\n setListenerXYZ(context, lx, ly, lz);\n\n pannerRef.current = panner;\n contextRef.current = context;\n\n setProcessingChain(el, [panner]);\n detachLiveAnalyser(el, tempId);\n\n setError(null);\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to create spatial panner\";\n setError(msg);\n pannerRef.current = null;\n contextRef.current = null;\n }\n\n return () => {\n const element = audioRef.current;\n if (element) {\n setProcessingChain(element, []);\n }\n pannerRef.current = null;\n contextRef.current = null;\n };\n }, [enabled, panningModel, distanceModel, refDistance, sx, sy, sz, lx, ly, lz, audioRef]);\n\n const setSourcePosition = useCallback((x: number, y: number, z: number) => {\n const panner = pannerRef.current;\n if (panner) {\n setPannerPosition(panner, x, y, z);\n }\n }, []);\n\n const setListenerPosition = useCallback((x: number, y: number, z: number) => {\n const ctx = contextRef.current;\n if (ctx) {\n setListenerXYZ(ctx, x, y, z);\n }\n }, []);\n\n const setPanningModel = useCallback((model: PanningModelType) => {\n const panner = pannerRef.current;\n if (panner) {\n panner.panningModel = model;\n }\n }, []);\n\n return { setSourcePosition, setListenerPosition, setPanningModel, error };\n}\n"],"names":["setPannerPosition","panner","x","y","z","setListenerXYZ","context","useGingerSpatialAudio","options","enabled","panningModel","distanceModel","refDistance","position","listenerPosition","audioRef","useGinger","error","setError","useState","pannerRef","useRef","contextRef","sx","sy","sz","lx","ly","lz","useEffect","el","setProcessingChain","attached","attachLiveAnalyser","tempId","detachLiveAnalyser","msg","element","setSourcePosition","useCallback","setListenerPosition","ctx","setPanningModel","model"],"mappings":"4LAiCA,SAASA,EAAkBC,EAAoBC,EAAWC,EAAWC,EAAiB,CACpFH,EAAO,UAAU,MAAQC,EACzBD,EAAO,UAAU,MAAQE,EACzBF,EAAO,UAAU,MAAQG,CAC3B,CAEA,SAASC,EAAeC,EAAuBJ,EAAWC,EAAWC,EAAiB,CACpFE,EAAQ,SAAS,UAAU,MAAQJ,EACnCI,EAAQ,SAAS,UAAU,MAAQH,EACnCG,EAAQ,SAAS,UAAU,MAAQF,CACrC,CAYO,SAASG,EACdC,EAAwC,GACX,CAC7B,KAAM,CACJ,QAAAC,EAAU,GACV,aAAAC,EAAe,OACf,cAAAC,EAAgB,UAChB,YAAAC,EAAc,EACd,SAAAC,EAAW,CAAC,EAAG,EAAG,CAAC,EACnB,iBAAAC,EAAmB,CAAC,EAAG,EAAG,CAAC,CAAA,EACzBN,EACE,CAAE,SAAAO,CAAA,EAAaC,YAAA,EACf,CAACC,EAAOC,CAAQ,EAAIC,EAAAA,SAAwB,IAAI,EAEhDC,EAAYC,EAAAA,OAA0B,IAAI,EAC1CC,EAAaD,EAAAA,OAA4B,IAAI,EAE7C,CAACE,EAAIC,EAAIC,CAAE,EAAIZ,EACf,CAACa,EAAIC,EAAIC,CAAE,EAAId,EAErBe,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAKf,EAAS,QACpB,GAAI,GAACe,GAAM,OAAO,OAAW,KAI7B,IAAI,CAACrB,EAAS,CACZsB,EAAAA,mBAAmBD,EAAI,EAAE,EACzBV,EAAU,QAAU,KACpBE,EAAW,QAAU,KACrB,MACF,CAEA,GAAI,CACF,MAAMU,EAAWC,EAAAA,mBAAmBH,EAAI,CACtC,QAAS,GACT,sBAAuB,EACvB,YAAa,KACb,YAAa,CAAA,CACd,EACK,CAAE,QAAAxB,EAAS,GAAI4B,CAAA,EAAWF,EAE1B/B,EAASK,EAAQ,aAAA,EACvBL,EAAO,aAAeS,EACtBT,EAAO,cAAgBU,EACvBV,EAAO,YAAcW,EACrBZ,EAAkBC,EAAQsB,EAAIC,EAAIC,CAAE,EACpCpB,EAAeC,EAASoB,EAAIC,EAAIC,CAAE,EAElCR,EAAU,QAAUnB,EACpBqB,EAAW,QAAUhB,EAErByB,qBAAmBD,EAAI,CAAC7B,CAAM,CAAC,EAC/BkC,EAAAA,mBAAmBL,EAAII,CAAM,EAE7BhB,EAAS,IAAI,CACf,OAAS,EAAG,CACV,MAAMkB,EAAM,aAAa,MAAQ,EAAE,QAAU,kCAC7ClB,EAASkB,CAAG,EACZhB,EAAU,QAAU,KACpBE,EAAW,QAAU,IACvB,CAEA,MAAO,IAAM,CACX,MAAMe,EAAUtB,EAAS,QACrBsB,GACFN,EAAAA,mBAAmBM,EAAS,EAAE,EAEhCjB,EAAU,QAAU,KACpBE,EAAW,QAAU,IACvB,EACF,EAAG,CAACb,EAASC,EAAcC,EAAeC,EAAaW,EAAIC,EAAIC,EAAIC,EAAIC,EAAIC,EAAIb,CAAQ,CAAC,EAExF,MAAMuB,EAAoBC,EAAAA,YAAY,CAACrC,EAAWC,EAAWC,IAAc,CACzE,MAAMH,EAASmB,EAAU,QACrBnB,GACFD,EAAkBC,EAAQC,EAAGC,EAAGC,CAAC,CAErC,EAAG,CAAA,CAAE,EAECoC,EAAsBD,EAAAA,YAAY,CAACrC,EAAWC,EAAWC,IAAc,CAC3E,MAAMqC,EAAMnB,EAAW,QACnBmB,GACFpC,EAAeoC,EAAKvC,EAAGC,EAAGC,CAAC,CAE/B,EAAG,CAAA,CAAE,EAECsC,EAAkBH,cAAaI,GAA4B,CAC/D,MAAM1C,EAASmB,EAAU,QACrBnB,IACFA,EAAO,aAAe0C,EAE1B,EAAG,CAAA,CAAE,EAEL,MAAO,CAAE,kBAAAL,EAAmB,oBAAAE,EAAqB,gBAAAE,EAAiB,MAAAzB,CAAA,CACpE"}
@@ -0,0 +1,3 @@
1
+ export { useGingerSpatialAudio } from './useGingerSpatialAudio';
2
+ export type { SpatialPosition, UseGingerSpatialAudioOptions, UseGingerSpatialAudioResult, } from './useGingerSpatialAudio';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/spatial/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EACV,eAAe,EACf,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,yBAAyB,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { useState as X, useRef as R, useEffect as Y, useCallback as p } from "react";
2
+ import { s as d, a as Z, d as w } from "../liveAudioGraph-DvPaxBCP.js";
3
+ import { u as F } from "../useGinger-hpp2pAGY.js";
4
+ function b(t, o, i, c) {
5
+ t.positionX.value = o, t.positionY.value = i, t.positionZ.value = c;
6
+ }
7
+ function D(t, o, i, c) {
8
+ t.listener.positionX.value = o, t.listener.positionY.value = i, t.listener.positionZ.value = c;
9
+ }
10
+ function H(t = {}) {
11
+ const {
12
+ enabled: o = !0,
13
+ panningModel: i = "HRTF",
14
+ distanceModel: c = "inverse",
15
+ refDistance: m = 1,
16
+ position: L = [0, 0, 0],
17
+ listenerPosition: S = [0, 0, 0]
18
+ } = t, { audioRef: f } = F(), [z, g] = X(null), l = R(null), u = R(null), [v, P, h] = L, [y, M, x] = S;
19
+ Y(() => {
20
+ const e = f.current;
21
+ if (!(!e || typeof window > "u")) {
22
+ if (!o) {
23
+ d(e, []), l.current = null, u.current = null;
24
+ return;
25
+ }
26
+ try {
27
+ const n = Z(e, {
28
+ fftSize: 32,
29
+ smoothingTimeConstant: 0,
30
+ minDecibels: -100,
31
+ maxDecibels: 0
32
+ }), { context: s, id: r } = n, a = s.createPanner();
33
+ a.panningModel = i, a.distanceModel = c, a.refDistance = m, b(a, v, P, h), D(s, y, M, x), l.current = a, u.current = s, d(e, [a]), w(e, r), g(null);
34
+ } catch (n) {
35
+ const s = n instanceof Error ? n.message : "Failed to create spatial panner";
36
+ g(s), l.current = null, u.current = null;
37
+ }
38
+ return () => {
39
+ const n = f.current;
40
+ n && d(n, []), l.current = null, u.current = null;
41
+ };
42
+ }
43
+ }, [o, i, c, m, v, P, h, y, M, x, f]);
44
+ const A = p((e, n, s) => {
45
+ const r = l.current;
46
+ r && b(r, e, n, s);
47
+ }, []), C = p((e, n, s) => {
48
+ const r = u.current;
49
+ r && D(r, e, n, s);
50
+ }, []), E = p((e) => {
51
+ const n = l.current;
52
+ n && (n.panningModel = e);
53
+ }, []);
54
+ return { setSourcePosition: A, setListenerPosition: C, setPanningModel: E, error: z };
55
+ }
56
+ export {
57
+ H as useGingerSpatialAudio
58
+ };
59
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/spatial/useGingerSpatialAudio.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n attachLiveAnalyser,\n detachLiveAnalyser,\n setProcessingChain,\n} from \"../analyzer/liveAudioGraph\";\nimport { useGinger } from \"../hooks/useGinger\";\n\n/** `[x, y, z]` in Web Audio space (meters). */\nexport type SpatialPosition = [number, number, number];\n\nexport type UseGingerSpatialAudioOptions = {\n /** When false, the panner is removed and the audio path is bypassed. Default: true. */\n enabled?: boolean;\n /** Default: `\"HRTF\"`. */\n panningModel?: PanningModelType;\n /** Default: `\"inverse\"`. */\n distanceModel?: DistanceModelType;\n /** Reference distance for attenuation. Default: `1`. */\n refDistance?: number;\n /** Source position. Default: `[0, 0, 0]`. */\n position?: SpatialPosition;\n /** Listener position. Default: `[0, 0, 0]`. */\n listenerPosition?: SpatialPosition;\n};\n\nexport type UseGingerSpatialAudioResult = {\n setSourcePosition: (x: number, y: number, z: number) => void;\n setListenerPosition: (x: number, y: number, z: number) => void;\n setPanningModel: (model: PanningModelType) => void;\n error: string | null;\n};\n\nfunction setPannerPosition(panner: PannerNode, x: number, y: number, z: number): void {\n panner.positionX.value = x;\n panner.positionY.value = y;\n panner.positionZ.value = z;\n}\n\nfunction setListenerXYZ(context: AudioContext, x: number, y: number, z: number): void {\n context.listener.positionX.value = x;\n context.listener.positionY.value = y;\n context.listener.positionZ.value = z;\n}\n\n/**\n * Inserts an HRTF `PannerNode` into the Web Audio graph for the active Ginger media element.\n *\n * Shares the same `AudioContext` as `useGingerEqualizer` / `useGingerLiveAnalyzer` — only one\n * `MediaElementAudioSourceNode` per element is allowed by the browser.\n *\n * ```ts\n * import { useGingerSpatialAudio } from \"@lucaismyname/ginger/spatial\";\n * ```\n */\nexport function useGingerSpatialAudio(\n options: UseGingerSpatialAudioOptions = {},\n): UseGingerSpatialAudioResult {\n const {\n enabled = true,\n panningModel = \"HRTF\",\n distanceModel = \"inverse\",\n refDistance = 1,\n position = [0, 0, 0],\n listenerPosition = [0, 0, 0],\n } = options;\n const { audioRef } = useGinger();\n const [error, setError] = useState<string | null>(null);\n\n const pannerRef = useRef<PannerNode | null>(null);\n const contextRef = useRef<AudioContext | null>(null);\n\n const [sx, sy, sz] = position;\n const [lx, ly, lz] = listenerPosition;\n\n useEffect(() => {\n const el = audioRef.current;\n if (!el || typeof window === \"undefined\") {\n return;\n }\n\n if (!enabled) {\n setProcessingChain(el, []);\n pannerRef.current = null;\n contextRef.current = null;\n return;\n }\n\n try {\n const attached = attachLiveAnalyser(el, {\n fftSize: 32,\n smoothingTimeConstant: 0,\n minDecibels: -100,\n maxDecibels: 0,\n });\n const { context, id: tempId } = attached;\n\n const panner = context.createPanner();\n panner.panningModel = panningModel;\n panner.distanceModel = distanceModel;\n panner.refDistance = refDistance;\n setPannerPosition(panner, sx, sy, sz);\n setListenerXYZ(context, lx, ly, lz);\n\n pannerRef.current = panner;\n contextRef.current = context;\n\n setProcessingChain(el, [panner]);\n detachLiveAnalyser(el, tempId);\n\n setError(null);\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to create spatial panner\";\n setError(msg);\n pannerRef.current = null;\n contextRef.current = null;\n }\n\n return () => {\n const element = audioRef.current;\n if (element) {\n setProcessingChain(element, []);\n }\n pannerRef.current = null;\n contextRef.current = null;\n };\n }, [enabled, panningModel, distanceModel, refDistance, sx, sy, sz, lx, ly, lz, audioRef]);\n\n const setSourcePosition = useCallback((x: number, y: number, z: number) => {\n const panner = pannerRef.current;\n if (panner) {\n setPannerPosition(panner, x, y, z);\n }\n }, []);\n\n const setListenerPosition = useCallback((x: number, y: number, z: number) => {\n const ctx = contextRef.current;\n if (ctx) {\n setListenerXYZ(ctx, x, y, z);\n }\n }, []);\n\n const setPanningModel = useCallback((model: PanningModelType) => {\n const panner = pannerRef.current;\n if (panner) {\n panner.panningModel = model;\n }\n }, []);\n\n return { setSourcePosition, setListenerPosition, setPanningModel, error };\n}\n"],"names":["setPannerPosition","panner","x","y","z","setListenerXYZ","context","useGingerSpatialAudio","options","enabled","panningModel","distanceModel","refDistance","position","listenerPosition","audioRef","useGinger","error","setError","useState","pannerRef","useRef","contextRef","sx","sy","sz","lx","ly","lz","useEffect","el","setProcessingChain","attached","attachLiveAnalyser","tempId","detachLiveAnalyser","e","msg","element","setSourcePosition","useCallback","setListenerPosition","ctx","setPanningModel","model"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /** `[x, y, z]` in Web Audio space (meters). */
2
+ export type SpatialPosition = [number, number, number];
3
+ export type UseGingerSpatialAudioOptions = {
4
+ /** When false, the panner is removed and the audio path is bypassed. Default: true. */
5
+ enabled?: boolean;
6
+ /** Default: `"HRTF"`. */
7
+ panningModel?: PanningModelType;
8
+ /** Default: `"inverse"`. */
9
+ distanceModel?: DistanceModelType;
10
+ /** Reference distance for attenuation. Default: `1`. */
11
+ refDistance?: number;
12
+ /** Source position. Default: `[0, 0, 0]`. */
13
+ position?: SpatialPosition;
14
+ /** Listener position. Default: `[0, 0, 0]`. */
15
+ listenerPosition?: SpatialPosition;
16
+ };
17
+ export type UseGingerSpatialAudioResult = {
18
+ setSourcePosition: (x: number, y: number, z: number) => void;
19
+ setListenerPosition: (x: number, y: number, z: number) => void;
20
+ setPanningModel: (model: PanningModelType) => void;
21
+ error: string | null;
22
+ };
23
+ /**
24
+ * Inserts an HRTF `PannerNode` into the Web Audio graph for the active Ginger media element.
25
+ *
26
+ * Shares the same `AudioContext` as `useGingerEqualizer` / `useGingerLiveAnalyzer` — only one
27
+ * `MediaElementAudioSourceNode` per element is allowed by the browser.
28
+ *
29
+ * ```ts
30
+ * import { useGingerSpatialAudio } from "@lucaismyname/ginger/spatial";
31
+ * ```
32
+ */
33
+ export declare function useGingerSpatialAudio(options?: UseGingerSpatialAudioOptions): UseGingerSpatialAudioResult;
34
+ //# sourceMappingURL=useGingerSpatialAudio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerSpatialAudio.d.ts","sourceRoot":"","sources":["../../src/spatial/useGingerSpatialAudio.ts"],"names":[],"mappings":"AAQA,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAEvD,MAAM,MAAM,4BAA4B,GAAG;IACzC,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yBAAyB;IACzB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,4BAA4B;IAC5B,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,eAAe,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,iBAAiB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,mBAAmB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/D,eAAe,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACnD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAcF;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,CA6F7B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useGingerSpatialAudio.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerSpatialAudio.test.d.ts","sourceRoot":"","sources":["../../src/spatial/useGingerSpatialAudio.test.tsx"],"names":[],"mappings":""}
@@ -33,11 +33,24 @@ export declare class MockBiquadFilterNode extends MockAudioNode {
33
33
  value: number;
34
34
  };
35
35
  }
36
+ declare class MockAudioParam {
37
+ value: number;
38
+ }
39
+ export declare class MockPannerNode extends MockAudioNode {
40
+ panningModel: PanningModelType;
41
+ distanceModel: DistanceModelType;
42
+ refDistance: number;
43
+ readonly positionX: MockAudioParam;
44
+ readonly positionY: MockAudioParam;
45
+ readonly positionZ: MockAudioParam;
46
+ }
36
47
  export declare class MockAudioContext extends EventTarget {
37
48
  readonly destination: MockAudioDestinationNode;
49
+ readonly listener: AudioListener;
38
50
  readonly sources: MockMediaElementAudioSourceNode[];
39
51
  readonly analysers: MockAnalyserNode[];
40
52
  readonly biquadFilters: MockBiquadFilterNode[];
53
+ readonly panners: MockPannerNode[];
41
54
  sampleRate: number;
42
55
  state: MockAudioContextState;
43
56
  closeCalls: number;
@@ -45,6 +58,7 @@ export declare class MockAudioContext extends EventTarget {
45
58
  createMediaElementSource(element: HTMLAudioElement): MediaElementAudioSourceNode;
46
59
  createAnalyser(): AnalyserNode;
47
60
  createBiquadFilter(): BiquadFilterNode;
61
+ createPanner(): PannerNode;
48
62
  resume(): Promise<void>;
49
63
  close(): Promise<void>;
50
64
  setStateForTest(nextState: MockAudioContextState): void;
@@ -1 +1 @@
1
- {"version":3,"file":"mockWebAudio.d.ts","sourceRoot":"","sources":["../../src/testing/mockWebAudio.ts"],"names":[],"mappings":"AAAA,KAAK,qBAAqB,GAAG,iBAAiB,CAAC;AAE/C,cAAM,aAAa;IACjB,QAAQ,CAAC,WAAW,EAAE,aAAa,EAAE,CAAM;IAC3C,QAAQ,CAAC,YAAY,EAAE,aAAa,EAAE,CAAM;IAC5C,eAAe,SAAK;IAEpB,OAAO,CAAC,IAAI,EAAE,aAAa;IAM3B,UAAU,CAAC,KAAK,CAAC,EAAE,aAAa;CAIjC;AAED,qBAAa,wBAAyB,SAAQ,aAAa;CAAG;AAE9D,qBAAa,+BAAgC,SAAQ,aAAa;IACpD,QAAQ,CAAC,YAAY,EAAE,gBAAgB;gBAA9B,YAAY,EAAE,gBAAgB;CAGpD;AAED,qBAAa,gBAAiB,SAAQ,aAAa;IACjD,OAAO,SAAQ;IACf,qBAAqB,SAAO;IAC5B,WAAW,SAAQ;IACnB,WAAW,SAAO;IAElB,IAAI,iBAAiB,WAEpB;IAED,oBAAoB,CAAC,KAAK,EAAE,UAAU;IAMtC,qBAAqB,CAAC,KAAK,EAAE,UAAU;CAKxC;AAED,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,IAAI,EAAE,gBAAgB,CAAa;IACnC,QAAQ,CAAC,SAAS;;MAAmB;IACrC,QAAQ,CAAC,IAAI;;MAAgB;IAC7B,QAAQ,CAAC,CAAC;;MAAgB;CAC3B;AAED,qBAAa,gBAAiB,SAAQ,WAAW;IAC/C,QAAQ,CAAC,WAAW,2BAAkC;IACtD,QAAQ,CAAC,OAAO,EAAE,+BAA+B,EAAE,CAAM;IACzD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,EAAE,CAAM;IAC5C,QAAQ,CAAC,aAAa,EAAE,oBAAoB,EAAE,CAAM;IAEpD,UAAU,SAAU;IACpB,KAAK,EAAE,qBAAqB,CAAa;IACzC,UAAU,SAAK;IACf,WAAW,SAAK;IAEhB,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,GAGpB,2BAA2B;IAGzD,cAAc,IAGkB,YAAY;IAG5C,kBAAkB,IAGY,gBAAgB;IAGxC,MAAM;IAON,KAAK;IAKX,eAAe,CAAC,SAAS,EAAE,qBAAqB;CAKjD;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,mBAAmB,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,0BAA0B,EAAE,MAAM,MAAM,CAAC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,mBAAmB,IAAI,mBAAmB,CAgFzD"}
1
+ {"version":3,"file":"mockWebAudio.d.ts","sourceRoot":"","sources":["../../src/testing/mockWebAudio.ts"],"names":[],"mappings":"AAAA,KAAK,qBAAqB,GAAG,iBAAiB,CAAC;AAE/C,cAAM,aAAa;IACjB,QAAQ,CAAC,WAAW,EAAE,aAAa,EAAE,CAAM;IAC3C,QAAQ,CAAC,YAAY,EAAE,aAAa,EAAE,CAAM;IAC5C,eAAe,SAAK;IAEpB,OAAO,CAAC,IAAI,EAAE,aAAa;IAM3B,UAAU,CAAC,KAAK,CAAC,EAAE,aAAa;CAIjC;AAED,qBAAa,wBAAyB,SAAQ,aAAa;CAAG;AAE9D,qBAAa,+BAAgC,SAAQ,aAAa;IACpD,QAAQ,CAAC,YAAY,EAAE,gBAAgB;gBAA9B,YAAY,EAAE,gBAAgB;CAGpD;AAED,qBAAa,gBAAiB,SAAQ,aAAa;IACjD,OAAO,SAAQ;IACf,qBAAqB,SAAO;IAC5B,WAAW,SAAQ;IACnB,WAAW,SAAO;IAElB,IAAI,iBAAiB,WAEpB;IAED,oBAAoB,CAAC,KAAK,EAAE,UAAU;IAMtC,qBAAqB,CAAC,KAAK,EAAE,UAAU;CAKxC;AAED,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,IAAI,EAAE,gBAAgB,CAAa;IACnC,QAAQ,CAAC,SAAS;;MAAmB;IACrC,QAAQ,CAAC,IAAI;;MAAgB;IAC7B,QAAQ,CAAC,CAAC;;MAAgB;CAC3B;AAED,cAAM,cAAc;IAClB,KAAK,SAAK;CACX;AAQD,qBAAa,cAAe,SAAQ,aAAa;IAC/C,YAAY,EAAE,gBAAgB,CAAU;IACxC,aAAa,EAAE,iBAAiB,CAAa;IAC7C,WAAW,SAAK;IAChB,QAAQ,CAAC,SAAS,iBAAwB;IAC1C,QAAQ,CAAC,SAAS,iBAAwB;IAC1C,QAAQ,CAAC,SAAS,iBAAwB;CAC3C;AAED,qBAAa,gBAAiB,SAAQ,WAAW;IAC/C,QAAQ,CAAC,WAAW,2BAAkC;IACtD,QAAQ,CAAC,QAAQ,EAAyC,aAAa,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,+BAA+B,EAAE,CAAM;IACzD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,EAAE,CAAM;IAC5C,QAAQ,CAAC,aAAa,EAAE,oBAAoB,EAAE,CAAM;IACpD,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,CAAM;IAExC,UAAU,SAAU;IACpB,KAAK,EAAE,qBAAqB,CAAa;IACzC,UAAU,SAAK;IACf,WAAW,SAAK;IAEhB,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,GAGpB,2BAA2B;IAGzD,cAAc,IAGkB,YAAY;IAG5C,kBAAkB,IAGY,gBAAgB;IAG9C,YAAY,IAGkB,UAAU;IAGlC,MAAM;IAON,KAAK;IAKX,eAAe,CAAC,SAAS,EAAE,qBAAqB;CAKjD;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,mBAAmB,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,0BAA0B,EAAE,MAAM,MAAM,CAAC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,mBAAmB,IAAI,mBAAmB,CAgFzD"}
@@ -0,0 +1,8 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("react"),v=require("../GingerSplitContexts-C7puo0M7.cjs");function h(n){return n.replace(/<[^>]+>/g,"").trim()}function T(n){const r=n.trim().replace(",",".").split(":");if(r.length===3){const s=Number(r[0]),t=Number(r[1]),c=Number(r[2]);return[s,t,c].every(Number.isFinite)?s*3600+t*60+c:Number.NaN}if(r.length===2){const s=Number(r[0]),t=Number(r[1]);return[s,t].every(Number.isFinite)?s*60+t:Number.NaN}return Number.NaN}function S(n){const i=n.indexOf("-->");if(i<0)return null;const r=n.slice(0,i).trim(),t=n.slice(i+3).trim().match(/^(\S+)/);return t?{start:r,end:t[1]}:null}function N(n){const i=[],r=n.replace(/\r\n/g,`
2
+ `).split(/\n\s*\n/);for(const s of r){const t=s.split(`
3
+ `).map(a=>a.trim()).filter(a=>a.length>0);if(t.length===0)continue;let c=0;/^\d+$/.test(t[0])&&(c=1);const o=t[c];if(!o)continue;const e=S(o);if(!e)continue;const m=T(e.start),u=T(e.end);if(!Number.isFinite(m)||!Number.isFinite(u))continue;const l=t.slice(c+1),f=h(l.join(`
4
+ `));f&&i.push({startTime:m,endTime:u,text:f})}return i.sort((s,t)=>s.startTime-t.startTime)}function b(n){const i=[];let r=n.replace(/\r\n/g,`
5
+ `);if(r.startsWith("WEBVTT")){const t=r.search(/\n\s*\n/);r=t>=0?r.slice(t).trim():""}const s=r.split(/\n\s*\n/);for(const t of s){const o=t.split(`
6
+ `).map(p=>p.trimEnd());if(o.length===0||o[0].startsWith("NOTE")||o[0].startsWith("STYLE")||o[0].startsWith("REGION"))continue;let e=0,m,u=o[e];if(u.includes("-->")||(m=o[e],e+=1,u=o[e]),!(u!=null&&u.includes("-->")))continue;const l=S(u);if(!l)continue;const f=T(l.start),a=T(l.end);if(!Number.isFinite(f)||!Number.isFinite(a))continue;const F=o.slice(e+1).filter(p=>p.trim().length>0),g=h(F.join(`
7
+ `));g&&i.push({startTime:f,endTime:a,text:g,...m?{id:m}:{}})}return i.sort((t,c)=>t.startTime-c.startTime)}function y(n){return n.trimStart().startsWith("WEBVTT")?b(n):N(n)}function x(n,i){return i==="vtt"?b(n):i==="srt"?N(n):y(n)}function W(n){const{transcript:i,format:r="auto"}=n,{currentTime:s}=v.useGingerMedia(),t=d.useMemo(()=>Array.isArray(i)?[...i].filter(e=>Number.isFinite(e.startTime)&&Number.isFinite(e.endTime)&&e.startTime>=0&&e.endTime>=e.startTime).sort((e,m)=>e.startTime-m.startTime):x(i,r),[i,r]),c=d.useMemo(()=>{for(let e=t.length-1;e>=0;e-=1)if(s>=t[e].startTime)return e;return-1},[s,t]),o=d.useMemo(()=>t.filter(e=>s>=e.startTime&&s<e.endTime),[s,t]);return{cues:t,activeIndex:c,activeCue:c>=0?t[c]??null:null,activeCues:o}}exports.parseSrt=N;exports.parseTimestampToSeconds=T;exports.parseTranscriptAuto=y;exports.parseVtt=b;exports.useGingerTranscriptSync=W;
8
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../../src/transcript/parseTranscript.ts","../../src/transcript/useGingerTranscriptSync.ts"],"sourcesContent":["/**\n * A single timed transcript cue (SRT / WebVTT).\n */\nexport type TranscriptCue = {\n /** Start time in seconds. */\n startTime: number;\n /** End time in seconds. */\n endTime: number;\n text: string;\n /** Present for WebVTT cues that declare an identifier line. */\n id?: string;\n};\n\nfunction stripHtmlTags(text: string): string {\n return text.replace(/<[^>]+>/g, \"\").trim();\n}\n\n/** Parse `HH:MM:SS.mmm` / `HH:MM:SS,mmm` / `MM:SS.mmm` (WebVTT) to seconds. */\nexport function parseTimestampToSeconds(raw: string): number {\n const t = raw.trim().replace(\",\", \".\");\n const segs = t.split(\":\");\n if (segs.length === 3) {\n const h = Number(segs[0]);\n const m = Number(segs[1]);\n const sec = Number(segs[2]);\n if (![h, m, sec].every(Number.isFinite)) return Number.NaN;\n return h * 3600 + m * 60 + sec;\n }\n if (segs.length === 2) {\n const m = Number(segs[0]);\n const sec = Number(segs[1]);\n if (![m, sec].every(Number.isFinite)) return Number.NaN;\n return m * 60 + sec;\n }\n return Number.NaN;\n}\n\nfunction parseTimingLine(line: string): { start: string; end: string } | null {\n const arrow = line.indexOf(\"-->\");\n if (arrow < 0) return null;\n const start = line.slice(0, arrow).trim();\n const rest = line.slice(arrow + 3).trim();\n const endMatch = rest.match(/^(\\S+)/);\n if (!endMatch) return null;\n return { start, end: endMatch[1]! };\n}\n\n/**\n * Parse SubRip (`.srt`) content into ordered cues.\n */\nexport function parseSrt(srt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n const blocks = srt.replace(/\\r\\n/g, \"\\n\").split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const lines = block\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n if (lines.length === 0) continue;\n\n let i = 0;\n if (/^\\d+$/.test(lines[0]!)) {\n i = 1;\n }\n const timingLine = lines[i];\n if (!timingLine) continue;\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n const textLines = lines.slice(i + 1);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Parse WebVTT (`.vtt`) content into ordered cues. Ignores `NOTE` blocks and region/style headers.\n */\nexport function parseVtt(vtt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n let body = vtt.replace(/\\r\\n/g, \"\\n\");\n if (body.startsWith(\"WEBVTT\")) {\n const firstBlank = body.search(/\\n\\s*\\n/);\n body = firstBlank >= 0 ? body.slice(firstBlank).trim() : \"\";\n }\n\n const blocks = body.split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const rawLines = block.split(\"\\n\");\n const lines = rawLines.map((l) => l.trimEnd());\n if (lines.length === 0) continue;\n if (\n lines[0]!.startsWith(\"NOTE\") ||\n lines[0]!.startsWith(\"STYLE\") ||\n lines[0]!.startsWith(\"REGION\")\n ) {\n continue;\n }\n\n let i = 0;\n let id: string | undefined;\n let timingLine = lines[i]!;\n if (!timingLine.includes(\"-->\")) {\n id = lines[i]!;\n i += 1;\n timingLine = lines[i]!;\n }\n if (!timingLine?.includes(\"-->\")) continue;\n\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n\n const textLines = lines.slice(i + 1).filter((l) => l.trim().length > 0);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text, ...(id ? { id } : {}) });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Auto-detect format: WebVTT if the string starts with `WEBVTT`, otherwise SRT.\n */\nexport function parseTranscriptAuto(input: string): TranscriptCue[] {\n const trimmed = input.trimStart();\n if (trimmed.startsWith(\"WEBVTT\")) {\n return parseVtt(input);\n }\n return parseSrt(input);\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\nimport { type TranscriptCue, parseSrt, parseTranscriptAuto, parseVtt } from \"./parseTranscript\";\n\nexport type UseGingerTranscriptSyncOptions = {\n transcript: string | TranscriptCue[];\n /** Default: `\"auto\"` (WEBVTT header → VTT, else SRT). Ignored when `transcript` is a cue array. */\n format?: \"vtt\" | \"srt\" | \"auto\";\n};\n\nexport type GingerTranscriptSyncState = {\n cues: TranscriptCue[];\n /** Last cue index where `startTime <= currentTime` (same scan as lyrics sync). */\n activeIndex: number;\n activeCue: TranscriptCue | null;\n /** All cues active at `currentTime` (`startTime <= t < endTime`), including overlaps. */\n activeCues: TranscriptCue[];\n};\n\nfunction parseString(transcript: string, format: \"vtt\" | \"srt\" | \"auto\"): TranscriptCue[] {\n if (format === \"vtt\") return parseVtt(transcript);\n if (format === \"srt\") return parseSrt(transcript);\n return parseTranscriptAuto(transcript);\n}\n\n/**\n * Syncs SRT / WebVTT transcript cues to the current Ginger playback time.\n *\n * ```ts\n * import { useGingerTranscriptSync } from \"@lucaismyname/ginger/transcript\";\n * ```\n */\nexport function useGingerTranscriptSync(\n options: UseGingerTranscriptSyncOptions,\n): GingerTranscriptSyncState {\n const { transcript, format = \"auto\" } = options;\n const { currentTime } = useGingerMedia();\n\n const cues = useMemo(() => {\n if (Array.isArray(transcript)) {\n return [...transcript]\n .filter(\n (c) =>\n Number.isFinite(c.startTime) &&\n Number.isFinite(c.endTime) &&\n c.startTime >= 0 &&\n c.endTime >= c.startTime,\n )\n .sort((a, b) => a.startTime - b.startTime);\n }\n return parseString(transcript, format);\n }, [transcript, format]);\n\n const activeIndex = useMemo(() => {\n for (let i = cues.length - 1; i >= 0; i -= 1) {\n if (currentTime >= cues[i]!.startTime) return i;\n }\n return -1;\n }, [currentTime, cues]);\n\n const activeCues = useMemo(() => {\n return cues.filter((c) => currentTime >= c.startTime && currentTime < c.endTime);\n }, [currentTime, cues]);\n\n return {\n cues,\n activeIndex,\n activeCue: activeIndex >= 0 ? (cues[activeIndex] ?? null) : null,\n activeCues,\n };\n}\n"],"names":["stripHtmlTags","text","parseTimestampToSeconds","raw","segs","h","m","sec","parseTimingLine","line","arrow","start","endMatch","parseSrt","srt","cues","blocks","block","lines","l","i","timingLine","times","startTime","endTime","textLines","a","b","parseVtt","vtt","body","firstBlank","id","parseTranscriptAuto","input","parseString","transcript","format","useGingerTranscriptSync","options","currentTime","useGingerMedia","useMemo","c","activeIndex","activeCues"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { parseSrt, parseTimestampToSeconds, parseTranscriptAuto, parseVtt, } from './parseTranscript';
2
+ export type { TranscriptCue } from './parseTranscript';
3
+ export { useGingerTranscriptSync } from './useGingerTranscriptSync';
4
+ export type { GingerTranscriptSyncState, UseGingerTranscriptSyncOptions, } from './useGingerTranscriptSync';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transcript/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,uBAAuB,EACvB,mBAAmB,EACnB,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,YAAY,EACV,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,99 @@
1
+ import { useMemo as p } from "react";
2
+ import { u as S } from "../GingerSplitContexts-BzBExb95.js";
3
+ function d(n) {
4
+ return n.replace(/<[^>]+>/g, "").trim();
5
+ }
6
+ function T(n) {
7
+ const i = n.trim().replace(",", ".").split(":");
8
+ if (i.length === 3) {
9
+ const s = Number(i[0]), t = Number(i[1]), o = Number(i[2]);
10
+ return [s, t, o].every(Number.isFinite) ? s * 3600 + t * 60 + o : Number.NaN;
11
+ }
12
+ if (i.length === 2) {
13
+ const s = Number(i[0]), t = Number(i[1]);
14
+ return [s, t].every(Number.isFinite) ? s * 60 + t : Number.NaN;
15
+ }
16
+ return Number.NaN;
17
+ }
18
+ function h(n) {
19
+ const r = n.indexOf("-->");
20
+ if (r < 0) return null;
21
+ const i = n.slice(0, r).trim(), t = n.slice(r + 3).trim().match(/^(\S+)/);
22
+ return t ? { start: i, end: t[1] } : null;
23
+ }
24
+ function g(n) {
25
+ const r = [], i = n.replace(/\r\n/g, `
26
+ `).split(/\n\s*\n/);
27
+ for (const s of i) {
28
+ const t = s.split(`
29
+ `).map((a) => a.trim()).filter((a) => a.length > 0);
30
+ if (t.length === 0) continue;
31
+ let o = 0;
32
+ /^\d+$/.test(t[0]) && (o = 1);
33
+ const c = t[o];
34
+ if (!c) continue;
35
+ const e = h(c);
36
+ if (!e) continue;
37
+ const m = T(e.start), u = T(e.end);
38
+ if (!Number.isFinite(m) || !Number.isFinite(u)) continue;
39
+ const l = t.slice(o + 1), f = d(l.join(`
40
+ `));
41
+ f && r.push({ startTime: m, endTime: u, text: f });
42
+ }
43
+ return r.sort((s, t) => s.startTime - t.startTime);
44
+ }
45
+ function F(n) {
46
+ const r = [];
47
+ let i = n.replace(/\r\n/g, `
48
+ `);
49
+ if (i.startsWith("WEBVTT")) {
50
+ const t = i.search(/\n\s*\n/);
51
+ i = t >= 0 ? i.slice(t).trim() : "";
52
+ }
53
+ const s = i.split(/\n\s*\n/);
54
+ for (const t of s) {
55
+ const c = t.split(`
56
+ `).map((N) => N.trimEnd());
57
+ if (c.length === 0 || c[0].startsWith("NOTE") || c[0].startsWith("STYLE") || c[0].startsWith("REGION"))
58
+ continue;
59
+ let e = 0, m, u = c[e];
60
+ if (u.includes("-->") || (m = c[e], e += 1, u = c[e]), !(u != null && u.includes("-->"))) continue;
61
+ const l = h(u);
62
+ if (!l) continue;
63
+ const f = T(l.start), a = T(l.end);
64
+ if (!Number.isFinite(f) || !Number.isFinite(a)) continue;
65
+ const x = c.slice(e + 1).filter((N) => N.trim().length > 0), b = d(x.join(`
66
+ `));
67
+ b && r.push({ startTime: f, endTime: a, text: b, ...m ? { id: m } : {} });
68
+ }
69
+ return r.sort((t, o) => t.startTime - o.startTime);
70
+ }
71
+ function W(n) {
72
+ return n.trimStart().startsWith("WEBVTT") ? F(n) : g(n);
73
+ }
74
+ function v(n, r) {
75
+ return r === "vtt" ? F(n) : r === "srt" ? g(n) : W(n);
76
+ }
77
+ function L(n) {
78
+ const { transcript: r, format: i = "auto" } = n, { currentTime: s } = S(), t = p(() => Array.isArray(r) ? [...r].filter(
79
+ (e) => Number.isFinite(e.startTime) && Number.isFinite(e.endTime) && e.startTime >= 0 && e.endTime >= e.startTime
80
+ ).sort((e, m) => e.startTime - m.startTime) : v(r, i), [r, i]), o = p(() => {
81
+ for (let e = t.length - 1; e >= 0; e -= 1)
82
+ if (s >= t[e].startTime) return e;
83
+ return -1;
84
+ }, [s, t]), c = p(() => t.filter((e) => s >= e.startTime && s < e.endTime), [s, t]);
85
+ return {
86
+ cues: t,
87
+ activeIndex: o,
88
+ activeCue: o >= 0 ? t[o] ?? null : null,
89
+ activeCues: c
90
+ };
91
+ }
92
+ export {
93
+ g as parseSrt,
94
+ T as parseTimestampToSeconds,
95
+ W as parseTranscriptAuto,
96
+ F as parseVtt,
97
+ L as useGingerTranscriptSync
98
+ };
99
+ //# sourceMappingURL=index.js.map