@lucaismyname/ginger 0.0.31 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -5
- package/dist/client.cjs +1 -1
- package/dist/client.js +37 -36
- package/dist/client.js.map +1 -1
- package/dist/equalizer/index.cjs +1 -1
- package/dist/equalizer/index.cjs.map +1 -1
- package/dist/equalizer/index.js +16 -15
- package/dist/equalizer/index.js.map +1 -1
- package/dist/hooks/useNextTrackPrefetch.d.ts +3 -0
- package/dist/hooks/useNextTrackPrefetch.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +37 -36
- package/dist/index.js.map +1 -1
- package/dist/liveAudioGraph-0cpHD_Ic.cjs +2 -0
- package/dist/liveAudioGraph-0cpHD_Ic.cjs.map +1 -0
- package/dist/liveAudioGraph-DvPaxBCP.js +105 -0
- package/dist/liveAudioGraph-DvPaxBCP.js.map +1 -0
- package/dist/remote/index.cjs +2 -0
- package/dist/remote/index.cjs.map +1 -0
- package/dist/remote/index.d.ts +5 -0
- package/dist/remote/index.d.ts.map +1 -0
- package/dist/remote/index.js +168 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/remoteProtocol.d.ts +28 -0
- package/dist/remote/remoteProtocol.d.ts.map +1 -0
- package/dist/remote/useGingerRemote.d.ts +35 -0
- package/dist/remote/useGingerRemote.d.ts.map +1 -0
- package/dist/remote/useGingerRemote.test.d.ts +2 -0
- package/dist/remote/useGingerRemote.test.d.ts.map +1 -0
- package/dist/remote/validateGingerInitPayloadDev.d.ts +7 -0
- package/dist/remote/validateGingerInitPayloadDev.d.ts.map +1 -0
- package/dist/remote/validateGingerInitPayloadDev.test.d.ts +2 -0
- package/dist/remote/validateGingerInitPayloadDev.test.d.ts.map +1 -0
- package/dist/spatial/index.cjs +2 -0
- package/dist/spatial/index.cjs.map +1 -0
- package/dist/spatial/index.d.ts +3 -0
- package/dist/spatial/index.d.ts.map +1 -0
- package/dist/spatial/index.js +59 -0
- package/dist/spatial/index.js.map +1 -0
- package/dist/spatial/useGingerSpatialAudio.d.ts +34 -0
- package/dist/spatial/useGingerSpatialAudio.d.ts.map +1 -0
- package/dist/spatial/useGingerSpatialAudio.test.d.ts +2 -0
- package/dist/spatial/useGingerSpatialAudio.test.d.ts.map +1 -0
- package/dist/testing/mockWebAudio.d.ts +14 -0
- package/dist/testing/mockWebAudio.d.ts.map +1 -1
- package/dist/transcript/index.cjs +8 -0
- package/dist/transcript/index.cjs.map +1 -0
- package/dist/transcript/index.d.ts +5 -0
- package/dist/transcript/index.d.ts.map +1 -0
- package/dist/transcript/index.js +99 -0
- package/dist/transcript/index.js.map +1 -0
- package/dist/transcript/parseTranscript.d.ts +27 -0
- package/dist/transcript/parseTranscript.d.ts.map +1 -0
- package/dist/transcript/parseTranscript.test.d.ts +2 -0
- package/dist/transcript/parseTranscript.test.d.ts.map +1 -0
- package/dist/transcript/useGingerTranscriptSync.d.ts +23 -0
- package/dist/transcript/useGingerTranscriptSync.d.ts.map +1 -0
- package/dist/useGinger-BXgia32v.cjs +2 -0
- package/dist/useGinger-BXgia32v.cjs.map +1 -0
- package/dist/useGinger-hpp2pAGY.js +48 -0
- package/dist/useGinger-hpp2pAGY.js.map +1 -0
- package/dist/useGingerChapterProgress-BdaalJvX.cjs +2 -0
- package/dist/useGingerChapterProgress-BdaalJvX.cjs.map +1 -0
- package/dist/{useGingerChapterProgress-DLYdGytK.js → useGingerChapterProgress-CZdv-HiI.js} +23 -22
- package/dist/useGingerChapterProgress-CZdv-HiI.js.map +1 -0
- package/dist/waveform/analyzeAudioFile.d.ts.map +1 -1
- package/dist/waveform/getAudioContextConstructor.d.ts +6 -0
- package/dist/waveform/getAudioContextConstructor.d.ts.map +1 -0
- package/dist/waveform/index.cjs +1 -1
- package/dist/waveform/index.cjs.map +1 -1
- package/dist/waveform/index.js +162 -153
- package/dist/waveform/index.js.map +1 -1
- package/dist/waveform/useAudioFileAnalysis.d.ts +1 -0
- package/dist/waveform/useAudioFileAnalysis.d.ts.map +1 -1
- package/dist/waveform/useAudioPeaks.d.ts.map +1 -1
- package/package.json +17 -2
- package/dist/liveAudioGraph-CmEsdLgZ.js +0 -150
- package/dist/liveAudioGraph-CmEsdLgZ.js.map +0 -1
- package/dist/liveAudioGraph-D1BXMv_u.cjs +0 -2
- package/dist/liveAudioGraph-D1BXMv_u.cjs.map +0 -1
- package/dist/useGingerChapterProgress-BOqUimE7.cjs +0 -2
- package/dist/useGingerChapterProgress-BOqUimE7.cjs.map +0 -1
- package/dist/useGingerChapterProgress-DLYdGytK.js.map +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const r = /* @__PURE__ */ new WeakMap();
|
|
2
|
+
function d(t) {
|
|
3
|
+
const e = 2 ** Math.round(Math.log2(t));
|
|
4
|
+
return Math.min(32768, Math.max(32, e));
|
|
5
|
+
}
|
|
6
|
+
function h(t) {
|
|
7
|
+
const { processingChain: e } = t;
|
|
8
|
+
return e.length > 0 ? e[e.length - 1] : t.source;
|
|
9
|
+
}
|
|
10
|
+
function a(t) {
|
|
11
|
+
const { source: e, processingChain: n, consumers: s, context: c } = t;
|
|
12
|
+
try {
|
|
13
|
+
e.disconnect();
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
for (const o of n)
|
|
17
|
+
try {
|
|
18
|
+
o.disconnect();
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
for (const { analyser: o } of s.values())
|
|
22
|
+
try {
|
|
23
|
+
o.disconnect(c.destination);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
if (n.length > 0) {
|
|
27
|
+
e.connect(n[0]);
|
|
28
|
+
for (let o = 0; o < n.length - 1; o++)
|
|
29
|
+
n[o].connect(n[o + 1]);
|
|
30
|
+
}
|
|
31
|
+
const i = h(t);
|
|
32
|
+
if (s.size === 0)
|
|
33
|
+
i.connect(c.destination);
|
|
34
|
+
else {
|
|
35
|
+
let o = !0;
|
|
36
|
+
for (const { analyser: u } of s.values())
|
|
37
|
+
i.connect(u), o && (u.connect(c.destination), o = !1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function f(t) {
|
|
41
|
+
let e = r.get(t);
|
|
42
|
+
if (!e) {
|
|
43
|
+
const n = window.AudioContext ?? window.webkitAudioContext;
|
|
44
|
+
if (!n)
|
|
45
|
+
throw new Error("Web Audio API is not available");
|
|
46
|
+
const s = new n(), c = s.createMediaElementSource(t);
|
|
47
|
+
e = {
|
|
48
|
+
context: s,
|
|
49
|
+
source: c,
|
|
50
|
+
consumers: /* @__PURE__ */ new Map(),
|
|
51
|
+
nextId: 0,
|
|
52
|
+
processingChain: [],
|
|
53
|
+
processingActive: !1
|
|
54
|
+
}, r.set(t, e);
|
|
55
|
+
}
|
|
56
|
+
return e;
|
|
57
|
+
}
|
|
58
|
+
function l(t, e) {
|
|
59
|
+
if (e.consumers.size === 0 && !e.processingActive) {
|
|
60
|
+
try {
|
|
61
|
+
e.source.disconnect();
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
e.context.close(), r.delete(t);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function g(t, e) {
|
|
68
|
+
const n = f(t), { context: s } = n, c = s.createAnalyser();
|
|
69
|
+
c.fftSize = d(e.fftSize), c.smoothingTimeConstant = e.smoothingTimeConstant, c.minDecibels = e.minDecibels, c.maxDecibels = e.maxDecibels;
|
|
70
|
+
const i = n.nextId;
|
|
71
|
+
return n.nextId += 1, n.consumers.set(i, { analyser: c }), a(n), { id: i, context: s, analyser: c };
|
|
72
|
+
}
|
|
73
|
+
function m(t, e) {
|
|
74
|
+
const n = r.get(t);
|
|
75
|
+
if (!n) return;
|
|
76
|
+
const s = n.consumers.get(e);
|
|
77
|
+
if (s) {
|
|
78
|
+
try {
|
|
79
|
+
s.analyser.disconnect();
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
if (n.consumers.delete(e), n.consumers.size === 0 && !n.processingActive) {
|
|
83
|
+
l(t, n);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
a(n);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function y(t, e) {
|
|
90
|
+
if (typeof window > "u") return;
|
|
91
|
+
if (e.length === 0) {
|
|
92
|
+
const s = r.get(t);
|
|
93
|
+
if (!s) return;
|
|
94
|
+
s.processingChain = [], s.processingActive = !1, s.consumers.size === 0 ? l(t, s) : a(s);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const n = f(t);
|
|
98
|
+
n.processingChain = e, n.processingActive = !0, a(n);
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
g as a,
|
|
102
|
+
m as d,
|
|
103
|
+
y as s
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=liveAudioGraph-DvPaxBCP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveAudioGraph-DvPaxBCP.js","sources":["../src/analyzer/liveAudioGraph.ts"],"sourcesContent":["/**\n * One MediaElementAudioSourceNode per HTMLAudioElement; multiple AnalyserNodes may tap the source.\n * An optional processing chain (e.g. EQ filters) can be inserted between the source and the\n * analysers via `setProcessingChain`.\n */\n\nexport type LiveAnalyserOptions = {\n fftSize: number;\n smoothingTimeConstant: number;\n minDecibels: number;\n maxDecibels: number;\n};\n\ntype Consumer = {\n analyser: AnalyserNode;\n};\n\ntype ElementEntry = {\n context: AudioContext;\n source: MediaElementAudioSourceNode;\n consumers: Map<number, Consumer>;\n nextId: number;\n /** Ordered processing nodes (e.g. BiquadFilterNode[]) inserted between source and analysers. */\n processingChain: AudioNode[];\n /** True while a processing chain is installed; prevents context teardown when no consumers exist. */\n processingActive: boolean;\n};\n\nconst entries = new WeakMap<HTMLAudioElement, ElementEntry>();\n\nfunction clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(32768, Math.max(32, p));\n}\n\nfunction getChainTail(entry: ElementEntry): AudioNode {\n const { processingChain } = entry;\n return processingChain.length > 0 ? processingChain[processingChain.length - 1]! : entry.source;\n}\n\n/**\n * Rebuild all graph connections from scratch.\n * Call after any structural change (add/remove consumer, change processing chain).\n */\nfunction rebuildGraph(entry: ElementEntry): void {\n const { source, processingChain, consumers, context } = entry;\n\n // Disconnect source outputs\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n\n // Disconnect processing chain node outputs\n for (const node of processingChain) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n\n // Disconnect all analysers from destination (we will reconnect selectively below)\n for (const { analyser } of consumers.values()) {\n try {\n analyser.disconnect(context.destination);\n } catch {\n // ignore\n }\n }\n\n // Build processing chain: source → node[0] → ... → node[N]\n if (processingChain.length > 0) {\n source.connect(processingChain[0]!);\n for (let i = 0; i < processingChain.length - 1; i++) {\n processingChain[i]!.connect(processingChain[i + 1]!);\n }\n }\n\n const tail = getChainTail(entry);\n\n if (consumers.size === 0) {\n // No analyser consumers: route tail directly to destination so audio is audible\n tail.connect(context.destination);\n } else {\n // Connect tail to all analysers; first one routes to destination (playback sink)\n let isFirst = true;\n for (const { analyser } of consumers.values()) {\n tail.connect(analyser);\n if (isFirst) {\n analyser.connect(context.destination);\n isFirst = false;\n }\n }\n }\n}\n\nfunction getOrCreateEntry(element: HTMLAudioElement): ElementEntry {\n let entry = entries.get(element);\n if (!entry) {\n const Context =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const context = new Context();\n const source = context.createMediaElementSource(element);\n entry = {\n context,\n source,\n consumers: new Map(),\n nextId: 0,\n processingChain: [],\n processingActive: false,\n };\n entries.set(element, entry);\n }\n return entry;\n}\n\nfunction maybeCloseEntry(element: HTMLAudioElement, entry: ElementEntry): void {\n if (entry.consumers.size === 0 && !entry.processingActive) {\n try {\n entry.source.disconnect();\n } catch {\n // ignore\n }\n void entry.context.close();\n entries.delete(element);\n }\n}\n\nexport function attachLiveAnalyser(\n element: HTMLAudioElement,\n options: LiveAnalyserOptions,\n): { id: number; context: AudioContext; analyser: AnalyserNode } {\n const entry = getOrCreateEntry(element);\n const { context } = entry;\n\n const analyser = context.createAnalyser();\n analyser.fftSize = clampFftSize(options.fftSize);\n analyser.smoothingTimeConstant = options.smoothingTimeConstant;\n analyser.minDecibels = options.minDecibels;\n analyser.maxDecibels = options.maxDecibels;\n\n const id = entry.nextId;\n entry.nextId += 1;\n entry.consumers.set(id, { analyser });\n\n rebuildGraph(entry);\n\n return { id, context, analyser };\n}\n\nexport function detachLiveAnalyser(element: HTMLAudioElement, id: number): void {\n const entry = entries.get(element);\n if (!entry) return;\n\n const consumer = entry.consumers.get(id);\n if (!consumer) return;\n\n try {\n consumer.analyser.disconnect();\n } catch {\n // ignore\n }\n entry.consumers.delete(id);\n\n if (entry.consumers.size === 0 && !entry.processingActive) {\n maybeCloseEntry(element, entry);\n return;\n }\n\n rebuildGraph(entry);\n}\n\n/**\n * Insert an ordered processing chain (e.g. BiquadFilterNode[]) between the audio source and the\n * analyser consumers. Pass an empty array to clear the chain.\n *\n * Safe to call while analyser consumers are active; the graph is rebuilt immediately.\n * Note: because `createMediaElementSource` can only be called once per element, the EQ and live\n * analyser share the same AudioContext. Calling both for the same element is supported.\n */\nexport function setProcessingChain(element: HTMLAudioElement, nodes: AudioNode[]): void {\n if (typeof window === \"undefined\") return;\n\n if (nodes.length === 0) {\n const entry = entries.get(element);\n if (!entry) return;\n entry.processingChain = [];\n entry.processingActive = false;\n if (entry.consumers.size === 0) {\n maybeCloseEntry(element, entry);\n } else {\n rebuildGraph(entry);\n }\n return;\n }\n\n const entry = getOrCreateEntry(element);\n entry.processingChain = nodes;\n entry.processingActive = true;\n rebuildGraph(entry);\n}\n"],"names":["entries","clampFftSize","n","p","getChainTail","entry","processingChain","rebuildGraph","source","consumers","context","node","analyser","i","tail","isFirst","getOrCreateEntry","element","Context","maybeCloseEntry","attachLiveAnalyser","options","id","detachLiveAnalyser","consumer","setProcessingChain","nodes"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("react"),U=require("../useGinger-BXgia32v.cjs"),C="ginger-remote",B=u=>u==="off"||u==="all"||u==="one",j=u=>u==="playlist"||u==="single";function x(u){if(!u||typeof u!="object")return!1;const e=u;if(!Array.isArray(e.tracks))return!1;for(const d of e.tracks){if(!d||typeof d!="object")return!1;const E=d;if(typeof E.title!="string"||typeof E.fileUrl!="string")return!1}return!(e.currentIndex!==void 0&&typeof e.currentIndex!="number"||e.isPaused!==void 0&&typeof e.isPaused!="boolean"||e.isShuffled!==void 0&&typeof e.isShuffled!="boolean"||e.repeatMode!==void 0&&!B(e.repeatMode)||e.playbackMode!==void 0&&!j(e.playbackMode)||e.volume!==void 0&&typeof e.volume!="number"||e.muted!==void 0&&typeof e.muted!="boolean"||e.playbackRate!==void 0&&typeof e.playbackRate!="number"||e.playlistMeta!==void 0&&e.playlistMeta!==null&&typeof e.playlistMeta!="object")}function F(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`ginger-tab-${Math.random().toString(36).slice(2)}`}function q(u={}){const{channelName:e=C,heartbeatMs:d=2e3,electionTimeoutMs:E=300}=u,{state:a,init:M}=U.useGinger(),t=c.useRef("");t.current===""&&(t.current=F());const[p,b]=c.useState("pending"),[_,T]=c.useState(0),[O,w]=c.useState(null),r=c.useRef(p);r.current=p;const S=c.useRef(M);S.current=M;const k=c.useRef(null),l=c.useRef(new Set),R=c.useRef(Date.now()),m=c.useRef(null),i=c.useRef(null),A=c.useRef(null),g=c.useCallback(s=>{var o;(o=k.current)==null||o.postMessage(s)},[]),y=c.useCallback(()=>{m.current&&(clearTimeout(m.current),m.current=null)},[]),N=c.useCallback(()=>{i.current&&(clearInterval(i.current),i.current=null)},[]);c.useEffect(()=>{if(typeof window>"u"||typeof BroadcastChannel>"u"){w("BroadcastChannel is not available in this environment");return}w(null);const s=new BroadcastChannel(e);k.current=s,l.current=new Set([t.current]);const o=I=>{s.postMessage(I)},v=()=>{i.current&&clearInterval(i.current),i.current=setInterval(()=>{const I=t.current;l.current.add(I);const n=l.current.size;T(n),o({type:"HEARTBEAT",tabId:I,connectedCount:n})},d)},h=()=>{y(),N(),b("follower"),r.current="follower"},H=()=>{y(),b("leader"),r.current="leader",l.current.add(t.current),o({type:"LEADER_ANNOUNCE",tabId:t.current}),v()},D=()=>{y(),m.current=setTimeout(()=>{r.current==="pending"&&(b("leader"),r.current="leader",l.current.add(t.current),o({type:"LEADER_ANNOUNCE",tabId:t.current}),v())},E)},L=I=>{const n=I.data;if(!n||typeof n!="object"||!("type"in n))return;const f=t.current;switch(n.type){case"PING":{l.current.add(n.tabId),r.current==="leader"&&o({type:"PONG",tabId:f,leaderTabId:f});break}case"PONG":{n.leaderTabId&&n.leaderTabId!==f&&(h(),R.current=Date.now());break}case"LEADER_ANNOUNCE":{if(n.tabId===f)break;l.current.add(n.tabId),n.tabId<f?(h(),R.current=Date.now()):n.tabId>f&&(r.current==="pending"||r.current==="leader")&&H();break}case"LEADER_RESIGN":{if(n.tabId===f)break;R.current=Date.now(),r.current==="follower"&&(b("pending"),r.current="pending",o({type:"PING",tabId:f}),D());break}case"HEARTBEAT":{r.current==="follower"&&(R.current=Date.now(),T(n.connectedCount));break}case"STATE_SNAPSHOT":{if(r.current==="follower"&&n.tabId!==f){if(process.env.NODE_ENV!=="production"&&!x(n.snapshot)){console.warn("[@lucaismyname/ginger] ignored STATE_SNAPSHOT: invalid GingerInitPayload");break}S.current(n.snapshot)}break}}};s.addEventListener("message",L),o({type:"PING",tabId:t.current}),m.current=setTimeout(()=>{r.current==="pending"&&(b("leader"),r.current="leader",l.current.add(t.current),o({type:"LEADER_ANNOUNCE",tabId:t.current}),v())},E),A.current=setInterval(()=>{r.current==="follower"&&Date.now()-R.current>d*2&&(b("pending"),r.current="pending",o({type:"PING",tabId:t.current}),D())},d);const P=()=>{r.current==="leader"&&o({type:"LEADER_RESIGN",tabId:t.current})};return window.addEventListener("pagehide",P),()=>{window.removeEventListener("pagehide",P),y(),N(),A.current&&(clearInterval(A.current),A.current=null),s.removeEventListener("message",L),r.current==="leader"&&o({type:"LEADER_RESIGN",tabId:t.current}),s.close(),k.current=null}},[e,y,E,d,N]),c.useEffect(()=>{if(p!=="leader")return;const s={tracks:a.tracks,currentIndex:a.currentIndex,playlistMeta:a.playlistMeta,isPaused:a.isPaused,isShuffled:!1,repeatMode:a.repeatMode,playbackMode:a.playbackMode,volume:a.volume,muted:a.muted,playbackRate:a.playbackRate};g({type:"STATE_SNAPSHOT",tabId:t.current,snapshot:s})},[p,a.tracks,a.currentIndex,a.isPaused,a.repeatMode,a.playbackMode,a.playlistMeta,a.volume,a.muted,a.playbackRate,g]);const G=c.useCallback(()=>{y(),N(),b("leader"),r.current="leader",l.current.add(t.current),g({type:"LEADER_ANNOUNCE",tabId:t.current}),i.current&&clearInterval(i.current),i.current=setInterval(()=>{const s=t.current;l.current.add(s);const o=l.current.size;T(o),g({type:"HEARTBEAT",tabId:s,connectedCount:o})},d)},[y,d,g,N]);return{isLeader:p==="leader",isFollower:p==="follower",isPending:p==="pending",connectedTabs:_,claimLeadership:G,error:O}}exports.DEFAULT_REMOTE_CHANNEL_NAME=C;exports.useGingerRemote=q;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/remote/remoteProtocol.ts","../../src/remote/validateGingerInitPayloadDev.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 type { GingerInitPayload } from \"../types\";\n\nconst repeatOk = (v: unknown): v is \"off\" | \"all\" | \"one\" =>\n v === \"off\" || v === \"all\" || v === \"one\";\n\nconst playbackModeOk = (v: unknown): v is \"playlist\" | \"single\" =>\n v === \"playlist\" || v === \"single\";\n\n/**\n * Structural check for remote `STATE_SNAPSHOT` payloads. Used in development to catch\n * malformed cross-tab messages; production callers still rely on same-origin `BroadcastChannel`.\n */\nexport function validateGingerInitPayloadDev(snapshot: unknown): snapshot is GingerInitPayload {\n if (!snapshot || typeof snapshot !== \"object\") return false;\n const s = snapshot as Record<string, unknown>;\n if (!Array.isArray(s.tracks)) return false;\n for (const t of s.tracks) {\n if (!t || typeof t !== \"object\") return false;\n const tr = t as Record<string, unknown>;\n if (typeof tr.title !== \"string\" || typeof tr.fileUrl !== \"string\") return false;\n }\n if (s.currentIndex !== undefined && typeof s.currentIndex !== \"number\") return false;\n if (s.isPaused !== undefined && typeof s.isPaused !== \"boolean\") return false;\n if (s.isShuffled !== undefined && typeof s.isShuffled !== \"boolean\") return false;\n if (s.repeatMode !== undefined && !repeatOk(s.repeatMode)) return false;\n if (s.playbackMode !== undefined && !playbackModeOk(s.playbackMode)) return false;\n if (s.volume !== undefined && typeof s.volume !== \"number\") return false;\n if (s.muted !== undefined && typeof s.muted !== \"boolean\") return false;\n if (s.playbackRate !== undefined && typeof s.playbackRate !== \"number\") return false;\n if (\n s.playlistMeta !== undefined &&\n s.playlistMeta !== null &&\n typeof s.playlistMeta !== \"object\"\n ) {\n return false;\n }\n return true;\n}\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\";\nimport { validateGingerInitPayloadDev } from \"./validateGingerInitPayloadDev\";\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 if (\n process.env.NODE_ENV !== \"production\" &&\n !validateGingerInitPayloadDev(msg.snapshot)\n ) {\n console.warn(\n \"[@lucaismyname/ginger] ignored STATE_SNAPSHOT: invalid GingerInitPayload\",\n );\n break;\n }\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","repeatOk","v","playbackModeOk","validateGingerInitPayloadDev","snapshot","s","t","tr","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","claimLeadership"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_REMOTE_CHANNEL_NAME } from './remoteProtocol';
|
|
2
|
+
export type { RemoteMessage } from './remoteProtocol';
|
|
3
|
+
export { useGingerRemote } from './useGingerRemote';
|
|
4
|
+
export type { UseGingerRemoteOptions, UseGingerRemoteResult } from './useGingerRemote';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/remote/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useRef as f, useState as M, useCallback as T, useEffect as O } from "react";
|
|
2
|
+
import { u as B } from "../useGinger-hpp2pAGY.js";
|
|
3
|
+
const j = "ginger-remote", F = (o) => o === "off" || o === "all" || o === "one", z = (o) => o === "playlist" || o === "single";
|
|
4
|
+
function V(o) {
|
|
5
|
+
if (!o || typeof o != "object") return !1;
|
|
6
|
+
const e = o;
|
|
7
|
+
if (!Array.isArray(e.tracks)) return !1;
|
|
8
|
+
for (const s of e.tracks) {
|
|
9
|
+
if (!s || typeof s != "object") return !1;
|
|
10
|
+
const I = s;
|
|
11
|
+
if (typeof I.title != "string" || typeof I.fileUrl != "string") return !1;
|
|
12
|
+
}
|
|
13
|
+
return !(e.currentIndex !== void 0 && typeof e.currentIndex != "number" || e.isPaused !== void 0 && typeof e.isPaused != "boolean" || e.isShuffled !== void 0 && typeof e.isShuffled != "boolean" || e.repeatMode !== void 0 && !F(e.repeatMode) || e.playbackMode !== void 0 && !z(e.playbackMode) || e.volume !== void 0 && typeof e.volume != "number" || e.muted !== void 0 && typeof e.muted != "boolean" || e.playbackRate !== void 0 && typeof e.playbackRate != "number" || e.playlistMeta !== void 0 && e.playlistMeta !== null && typeof e.playlistMeta != "object");
|
|
14
|
+
}
|
|
15
|
+
function W() {
|
|
16
|
+
return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `ginger-tab-${Math.random().toString(36).slice(2)}`;
|
|
17
|
+
}
|
|
18
|
+
function J(o = {}) {
|
|
19
|
+
const {
|
|
20
|
+
channelName: e = j,
|
|
21
|
+
heartbeatMs: s = 2e3,
|
|
22
|
+
electionTimeoutMs: I = 300
|
|
23
|
+
} = o, { state: a, init: h } = B(), t = f("");
|
|
24
|
+
t.current === "" && (t.current = W());
|
|
25
|
+
const [p, b] = M("pending"), [G, k] = M(0), [H, D] = M(null), r = f(p);
|
|
26
|
+
r.current = p;
|
|
27
|
+
const S = f(h);
|
|
28
|
+
S.current = h;
|
|
29
|
+
const v = f(null), l = f(/* @__PURE__ */ new Set()), m = f(Date.now()), g = f(null), d = f(null), A = f(null), N = T((u) => {
|
|
30
|
+
var c;
|
|
31
|
+
(c = v.current) == null || c.postMessage(u);
|
|
32
|
+
}, []), y = T(() => {
|
|
33
|
+
g.current && (clearTimeout(g.current), g.current = null);
|
|
34
|
+
}, []), R = T(() => {
|
|
35
|
+
d.current && (clearInterval(d.current), d.current = null);
|
|
36
|
+
}, []);
|
|
37
|
+
O(() => {
|
|
38
|
+
if (typeof window > "u" || typeof BroadcastChannel > "u") {
|
|
39
|
+
D("BroadcastChannel is not available in this environment");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
D(null);
|
|
43
|
+
const u = new BroadcastChannel(e);
|
|
44
|
+
v.current = u, l.current = /* @__PURE__ */ new Set([t.current]);
|
|
45
|
+
const c = (E) => {
|
|
46
|
+
u.postMessage(E);
|
|
47
|
+
}, w = () => {
|
|
48
|
+
d.current && clearInterval(d.current), d.current = setInterval(() => {
|
|
49
|
+
const E = t.current;
|
|
50
|
+
l.current.add(E);
|
|
51
|
+
const n = l.current.size;
|
|
52
|
+
k(n), c({ type: "HEARTBEAT", tabId: E, connectedCount: n });
|
|
53
|
+
}, s);
|
|
54
|
+
}, L = () => {
|
|
55
|
+
y(), R(), b("follower"), r.current = "follower";
|
|
56
|
+
}, x = () => {
|
|
57
|
+
y(), b("leader"), r.current = "leader", l.current.add(t.current), c({ type: "LEADER_ANNOUNCE", tabId: t.current }), w();
|
|
58
|
+
}, P = () => {
|
|
59
|
+
y(), g.current = setTimeout(() => {
|
|
60
|
+
r.current === "pending" && (b("leader"), r.current = "leader", l.current.add(t.current), c({ type: "LEADER_ANNOUNCE", tabId: t.current }), w());
|
|
61
|
+
}, I);
|
|
62
|
+
}, _ = (E) => {
|
|
63
|
+
const n = E.data;
|
|
64
|
+
if (!n || typeof n != "object" || !("type" in n)) return;
|
|
65
|
+
const i = t.current;
|
|
66
|
+
switch (n.type) {
|
|
67
|
+
case "PING": {
|
|
68
|
+
l.current.add(n.tabId), r.current === "leader" && c({ type: "PONG", tabId: i, leaderTabId: i });
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "PONG": {
|
|
72
|
+
n.leaderTabId && n.leaderTabId !== i && (L(), m.current = Date.now());
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "LEADER_ANNOUNCE": {
|
|
76
|
+
if (n.tabId === i) break;
|
|
77
|
+
l.current.add(n.tabId), n.tabId < i ? (L(), m.current = Date.now()) : n.tabId > i && (r.current === "pending" || r.current === "leader") && x();
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "LEADER_RESIGN": {
|
|
81
|
+
if (n.tabId === i) break;
|
|
82
|
+
m.current = Date.now(), r.current === "follower" && (b("pending"), r.current = "pending", c({ type: "PING", tabId: i }), P());
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "HEARTBEAT": {
|
|
86
|
+
r.current === "follower" && (m.current = Date.now(), k(n.connectedCount));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "STATE_SNAPSHOT": {
|
|
90
|
+
if (r.current === "follower" && n.tabId !== i) {
|
|
91
|
+
if (process.env.NODE_ENV !== "production" && !V(n.snapshot)) {
|
|
92
|
+
console.warn(
|
|
93
|
+
"[@lucaismyname/ginger] ignored STATE_SNAPSHOT: invalid GingerInitPayload"
|
|
94
|
+
);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
S.current(n.snapshot);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
u.addEventListener("message", _), c({ type: "PING", tabId: t.current }), g.current = setTimeout(() => {
|
|
104
|
+
r.current === "pending" && (b("leader"), r.current = "leader", l.current.add(t.current), c({ type: "LEADER_ANNOUNCE", tabId: t.current }), w());
|
|
105
|
+
}, I), A.current = setInterval(() => {
|
|
106
|
+
r.current === "follower" && Date.now() - m.current > s * 2 && (b("pending"), r.current = "pending", c({ type: "PING", tabId: t.current }), P());
|
|
107
|
+
}, s);
|
|
108
|
+
const C = () => {
|
|
109
|
+
r.current === "leader" && c({ type: "LEADER_RESIGN", tabId: t.current });
|
|
110
|
+
};
|
|
111
|
+
return window.addEventListener("pagehide", C), () => {
|
|
112
|
+
window.removeEventListener("pagehide", C), y(), R(), A.current && (clearInterval(A.current), A.current = null), u.removeEventListener("message", _), r.current === "leader" && c({ type: "LEADER_RESIGN", tabId: t.current }), u.close(), v.current = null;
|
|
113
|
+
};
|
|
114
|
+
}, [e, y, I, s, R]), O(() => {
|
|
115
|
+
if (p !== "leader") return;
|
|
116
|
+
const u = {
|
|
117
|
+
tracks: a.tracks,
|
|
118
|
+
currentIndex: a.currentIndex,
|
|
119
|
+
playlistMeta: a.playlistMeta,
|
|
120
|
+
isPaused: a.isPaused,
|
|
121
|
+
/** Avoid `createInitialState` re-shuffling on followers; queue order is already canonical. */
|
|
122
|
+
isShuffled: !1,
|
|
123
|
+
repeatMode: a.repeatMode,
|
|
124
|
+
playbackMode: a.playbackMode,
|
|
125
|
+
volume: a.volume,
|
|
126
|
+
muted: a.muted,
|
|
127
|
+
playbackRate: a.playbackRate
|
|
128
|
+
};
|
|
129
|
+
N({
|
|
130
|
+
type: "STATE_SNAPSHOT",
|
|
131
|
+
tabId: t.current,
|
|
132
|
+
snapshot: u
|
|
133
|
+
});
|
|
134
|
+
}, [
|
|
135
|
+
p,
|
|
136
|
+
a.tracks,
|
|
137
|
+
a.currentIndex,
|
|
138
|
+
a.isPaused,
|
|
139
|
+
a.repeatMode,
|
|
140
|
+
a.playbackMode,
|
|
141
|
+
a.playlistMeta,
|
|
142
|
+
a.volume,
|
|
143
|
+
a.muted,
|
|
144
|
+
a.playbackRate,
|
|
145
|
+
N
|
|
146
|
+
]);
|
|
147
|
+
const U = T(() => {
|
|
148
|
+
y(), R(), b("leader"), r.current = "leader", l.current.add(t.current), N({ type: "LEADER_ANNOUNCE", tabId: t.current }), d.current && clearInterval(d.current), d.current = setInterval(() => {
|
|
149
|
+
const u = t.current;
|
|
150
|
+
l.current.add(u);
|
|
151
|
+
const c = l.current.size;
|
|
152
|
+
k(c), N({ type: "HEARTBEAT", tabId: u, connectedCount: c });
|
|
153
|
+
}, s);
|
|
154
|
+
}, [y, s, N, R]);
|
|
155
|
+
return {
|
|
156
|
+
isLeader: p === "leader",
|
|
157
|
+
isFollower: p === "follower",
|
|
158
|
+
isPending: p === "pending",
|
|
159
|
+
connectedTabs: G,
|
|
160
|
+
claimLeadership: U,
|
|
161
|
+
error: H
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
j as DEFAULT_REMOTE_CHANNEL_NAME,
|
|
166
|
+
J as useGingerRemote
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/remote/remoteProtocol.ts","../../src/remote/validateGingerInitPayloadDev.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 type { GingerInitPayload } from \"../types\";\n\nconst repeatOk = (v: unknown): v is \"off\" | \"all\" | \"one\" =>\n v === \"off\" || v === \"all\" || v === \"one\";\n\nconst playbackModeOk = (v: unknown): v is \"playlist\" | \"single\" =>\n v === \"playlist\" || v === \"single\";\n\n/**\n * Structural check for remote `STATE_SNAPSHOT` payloads. Used in development to catch\n * malformed cross-tab messages; production callers still rely on same-origin `BroadcastChannel`.\n */\nexport function validateGingerInitPayloadDev(snapshot: unknown): snapshot is GingerInitPayload {\n if (!snapshot || typeof snapshot !== \"object\") return false;\n const s = snapshot as Record<string, unknown>;\n if (!Array.isArray(s.tracks)) return false;\n for (const t of s.tracks) {\n if (!t || typeof t !== \"object\") return false;\n const tr = t as Record<string, unknown>;\n if (typeof tr.title !== \"string\" || typeof tr.fileUrl !== \"string\") return false;\n }\n if (s.currentIndex !== undefined && typeof s.currentIndex !== \"number\") return false;\n if (s.isPaused !== undefined && typeof s.isPaused !== \"boolean\") return false;\n if (s.isShuffled !== undefined && typeof s.isShuffled !== \"boolean\") return false;\n if (s.repeatMode !== undefined && !repeatOk(s.repeatMode)) return false;\n if (s.playbackMode !== undefined && !playbackModeOk(s.playbackMode)) return false;\n if (s.volume !== undefined && typeof s.volume !== \"number\") return false;\n if (s.muted !== undefined && typeof s.muted !== \"boolean\") return false;\n if (s.playbackRate !== undefined && typeof s.playbackRate !== \"number\") return false;\n if (\n s.playlistMeta !== undefined &&\n s.playlistMeta !== null &&\n typeof s.playlistMeta !== \"object\"\n ) {\n return false;\n }\n return true;\n}\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\";\nimport { validateGingerInitPayloadDev } from \"./validateGingerInitPayloadDev\";\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 if (\n process.env.NODE_ENV !== \"production\" &&\n !validateGingerInitPayloadDev(msg.snapshot)\n ) {\n console.warn(\n \"[@lucaismyname/ginger] ignored STATE_SNAPSHOT: invalid GingerInitPayload\",\n );\n break;\n }\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","repeatOk","v","playbackModeOk","validateGingerInitPayloadDev","snapshot","s","t","tr","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","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":"AAMA,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,CAgS3F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGingerRemote.test.d.ts","sourceRoot":"","sources":["../../src/remote/useGingerRemote.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GingerInitPayload } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Structural check for remote `STATE_SNAPSHOT` payloads. Used in development to catch
|
|
4
|
+
* malformed cross-tab messages; production callers still rely on same-origin `BroadcastChannel`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateGingerInitPayloadDev(snapshot: unknown): snapshot is GingerInitPayload;
|
|
7
|
+
//# sourceMappingURL=validateGingerInitPayloadDev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateGingerInitPayloadDev.d.ts","sourceRoot":"","sources":["../../src/remote/validateGingerInitPayloadDev.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAQlD;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,iBAAiB,CAyB7F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateGingerInitPayloadDev.test.d.ts","sourceRoot":"","sources":["../../src/remote/validateGingerInitPayloadDev.test.ts"],"names":[],"mappings":""}
|
|
@@ -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":"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"}
|
|
@@ -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
|