@lucaismyname/ginger 0.0.54 → 0.0.56
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 +37 -1
- package/dist/GingerSplitContexts-DQ3rESBu.js +78 -0
- package/dist/GingerSplitContexts-DQ3rESBu.js.map +1 -0
- package/dist/GingerSplitContexts-KSB0vQ5F.cjs +2 -0
- package/dist/GingerSplitContexts-KSB0vQ5F.cjs.map +1 -0
- package/dist/analyzer/useGingerLiveAnalyzer.d.ts.map +1 -1
- package/dist/client.cjs +1 -1
- package/dist/client.js +28 -26
- package/dist/context/GingerProvider.d.ts +1 -1
- package/dist/context/GingerProvider.d.ts.map +1 -1
- package/dist/context/GingerSplitContexts.d.ts +22 -2
- package/dist/context/GingerSplitContexts.d.ts.map +1 -1
- package/dist/crossfade/index.cjs +1 -1
- package/dist/crossfade/index.cjs.map +1 -1
- package/dist/crossfade/index.js +116 -99
- package/dist/crossfade/index.js.map +1 -1
- package/dist/crossfade/useGingerCrossfade.d.ts.map +1 -1
- package/dist/devtools/GingerDevtools.d.ts +2 -0
- package/dist/devtools/GingerDevtools.d.ts.map +1 -0
- package/dist/devtools/index.cjs +2 -0
- package/dist/devtools/index.cjs.map +1 -0
- package/dist/devtools/index.d.ts +3 -0
- package/dist/devtools/index.d.ts.map +1 -0
- package/dist/devtools/index.js +465 -0
- package/dist/devtools/index.js.map +1 -0
- package/dist/devtools/registry.d.ts +49 -0
- package/dist/devtools/registry.d.ts.map +1 -0
- package/dist/equalizer/index.cjs +1 -1
- package/dist/equalizer/index.js +1 -1
- package/dist/experimental-gapless/index.cjs +1 -1
- package/dist/experimental-gapless/index.js +1 -1
- package/dist/formatTime-DUWvzW21.js +9 -0
- package/dist/formatTime-DUWvzW21.js.map +1 -0
- package/dist/formatTime-fm_QClmG.cjs +2 -0
- package/dist/formatTime-fm_QClmG.cjs.map +1 -0
- package/dist/ginger-B-W73LQC.cjs +2 -0
- package/dist/ginger-B-W73LQC.cjs.map +1 -0
- package/dist/ginger-BHu7Ofna.js +2177 -0
- package/dist/ginger-BHu7Ofna.js.map +1 -0
- package/dist/hooks/useGingerKeyboardShortcuts.d.ts.map +1 -1
- package/dist/hooks/useSeekDrag.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -26
- package/dist/internal/formatTime.d.ts.map +1 -1
- package/dist/remote/index.cjs +1 -1
- package/dist/remote/index.js +1 -1
- package/dist/spatial/index.cjs +1 -1
- package/dist/spatial/index.js +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/testing/index.cjs +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/transcript/index.cjs +1 -1
- package/dist/transcript/index.js +1 -1
- package/dist/types.d.ts +26 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{useGinger-4uvPoChz.cjs → useGinger-BMRLzjmr.cjs} +2 -2
- package/dist/{useGinger-4uvPoChz.cjs.map → useGinger-BMRLzjmr.cjs.map} +1 -1
- package/dist/{useGinger-Dz0cPyD1.js → useGinger-DKrHZ4NU.js} +2 -2
- package/dist/{useGinger-Dz0cPyD1.js.map → useGinger-DKrHZ4NU.js.map} +1 -1
- package/dist/useGingerChapterProgress-DAIWjAtG.js +343 -0
- package/dist/useGingerChapterProgress-DAIWjAtG.js.map +1 -0
- package/dist/useGingerChapterProgress-DLx-LayX.cjs +2 -0
- package/dist/useGingerChapterProgress-DLx-LayX.cjs.map +1 -0
- package/package.json +6 -1
- package/dist/GingerSplitContexts-BzBExb95.js +0 -64
- package/dist/GingerSplitContexts-BzBExb95.js.map +0 -1
- package/dist/GingerSplitContexts-C7puo0M7.cjs +0 -2
- package/dist/GingerSplitContexts-C7puo0M7.cjs.map +0 -1
- package/dist/ginger-9lWCvbHv.cjs +0 -2
- package/dist/ginger-9lWCvbHv.cjs.map +0 -1
- package/dist/ginger-XgXdGRB-.js +0 -2049
- package/dist/ginger-XgXdGRB-.js.map +0 -1
- package/dist/useGingerChapterProgress-Cqa9_CyH.cjs +0 -2
- package/dist/useGingerChapterProgress-Cqa9_CyH.cjs.map +0 -1
- package/dist/useGingerChapterProgress-DgqqoY5F.js +0 -324
- package/dist/useGingerChapterProgress-DgqqoY5F.js.map +0 -1
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ For docs beyond this README, use the repository links below:
|
|
|
78
78
|
- `@lucaismyname/ginger/remote`
|
|
79
79
|
- `@lucaismyname/ginger/crossfade`
|
|
80
80
|
- `@lucaismyname/ginger/experimental-gapless`
|
|
81
|
+
- `@lucaismyname/ginger/devtools`
|
|
81
82
|
|
|
82
83
|
### Equalizer
|
|
83
84
|
|
|
@@ -233,6 +234,35 @@ function CrossfadeSetup() {
|
|
|
233
234
|
|
|
234
235
|
For lower-level integrations, the subpath also exports **`attachCrossfadeGraph`**, **`scheduleCrossfade`**, and **`teardownCrossfadeGraph`** plus the related graph/curve types. Like EQ and spatial audio, crossfade attaches to the active Ginger media graph and should be torn down when you unmount or switch playback strategies.
|
|
235
236
|
|
|
237
|
+
### Devtools (`@lucaismyname/ginger/devtools`)
|
|
238
|
+
|
|
239
|
+
A debugging overlay for inspecting and controlling Ginger audio players at runtime. Supports **multiple providers** on the same page via a global registry — place a single `<GingerDevtools />` anywhere in your app and it auto-discovers every active `<Ginger.Provider>`.
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import { GingerDevtools } from "@lucaismyname/ginger/devtools";
|
|
243
|
+
|
|
244
|
+
function App() {
|
|
245
|
+
return (
|
|
246
|
+
<>
|
|
247
|
+
<Ginger.Provider debugLabel="Main Player" initialTracks={tracks}>
|
|
248
|
+
{/* ... */}
|
|
249
|
+
</Ginger.Provider>
|
|
250
|
+
|
|
251
|
+
<Ginger.Provider debugLabel="Ambient" initialTracks={ambientTracks}>
|
|
252
|
+
{/* ... */}
|
|
253
|
+
</Ginger.Provider>
|
|
254
|
+
|
|
255
|
+
{/* Single devtools instance — discovers both providers */}
|
|
256
|
+
<GingerDevtools />
|
|
257
|
+
</>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The overlay provides **bidirectional controls**: you can play/pause, seek, change volume, adjust playback rate, toggle repeat/shuffle, and click tracks in the queue — all changes apply to the live player instantly. State changes from the player are reflected in the devtools panel in real-time.
|
|
263
|
+
|
|
264
|
+
The panel uses Tailwind CSS via CDN (injected on mount, removed on unmount) and renders in a portal so it does not interfere with your app's layout or styles. Use the `debugLabel` prop on `<Ginger.Provider>` to give each player a human-readable tab name.
|
|
265
|
+
|
|
236
266
|
### Experimental Notice
|
|
237
267
|
|
|
238
268
|
`@lucaismyname/ginger/experimental-gapless` is intentionally non-production.
|
|
@@ -547,6 +577,7 @@ Props:
|
|
|
547
577
|
| `mediaSession` | `boolean \| GingerMediaSessionOptions` | `false` | `true` enables default Media Session bridge; pass `{ seekForwardSeconds, seekBackwardSeconds, positionState }` for optional OS skip controls and timeline sync |
|
|
548
578
|
| `beforePlay` | `() => boolean \| Promise<boolean>` | `undefined` | Policy hook run before playback starts |
|
|
549
579
|
| `onPlayBlocked` | `() => void` | `undefined` | Called when `beforePlay` returns `false` |
|
|
580
|
+
| `retryOnError` | `boolean \| GingerRetryConfig` | `undefined` | Auto-retry on transient media errors (e.g. network failures) with exponential backoff. `true` uses defaults (`maxRetries: 3`, `delayMs: 1500`). |
|
|
550
581
|
| `persistence` | `{ get(key): unknown; set(key, value): void }` | `undefined` | Adapter for persisted playback settings and resume state |
|
|
551
582
|
| `hydrateOnMount` | `boolean` | `false` | Hydrate persisted values into initial provider state |
|
|
552
583
|
| `resumeOnTrackChange` | `boolean` | `false` | Restore/save per-track playback position |
|
|
@@ -564,6 +595,7 @@ Props:
|
|
|
564
595
|
| `onSeek` | `(timeSeconds: number) => void` | `undefined` | Fires whenever `seek()` is invoked |
|
|
565
596
|
| `dir` | `"ltr" \| "rtl" \| "auto"` | locale-derived | Explicit provider layout direction |
|
|
566
597
|
| `prevRestartThresholdSeconds` | `number` | `3` | Previous restarts current track when `currentTime > threshold`; set `0` to always skip |
|
|
598
|
+
| `debugLabel` | `string` | `undefined` | Human-readable label shown in devtools tabs when multiple providers exist |
|
|
567
599
|
|
|
568
600
|
### `Ginger.Player`
|
|
569
601
|
|
|
@@ -899,6 +931,8 @@ Example:
|
|
|
899
931
|
|
|
900
932
|
- **`useGingerPlayback()`** / **`useGingerMedia()`** — Subscribe to queue/transport vs time/volume/buffering separately so dense UIs re-render less often.
|
|
901
933
|
|
|
934
|
+
- **`useGingerTime()`** / **`useGingerMediaControls()`** — Granular media subscriptions: `useGingerTime()` provides only high-frequency fields (`currentTime`, `duration`, `bufferedFraction`, `isBuffering`, `errorMessage`); `useGingerMediaControls()` provides low-frequency fields (`volume`, `muted`, `playbackRate`) plus actions (`seek`, `setVolume`, etc.). Components that only need volume controls avoid re-renders on every time tick.
|
|
935
|
+
|
|
902
936
|
- **`useGingerState()`** — Merged `GingerState` only (no actions); use inside custom display components together with hooks above for controls.
|
|
903
937
|
|
|
904
938
|
- **Headless control bindings** (bind to your own components): **`useSeekBarBinding()`**, **`useVolumeSlider()`**, **`usePlayPauseBinding({ playAriaLabel?, pauseAriaLabel? })`**. Each returns props such as `value`, `min`, `max`, handlers, and `ariaLabel` / `ariaValueText` where relevant.
|
|
@@ -1205,6 +1239,7 @@ New callbacks available on `Ginger.Provider`:
|
|
|
1205
1239
|
|
|
1206
1240
|
- `dir?: "ltr" | "rtl" | "auto"` — explicit layout direction; takes priority over the automatic RTL heuristic derived from locale strings
|
|
1207
1241
|
- `prevRestartThresholdSeconds?: number` — pressing previous restarts the current track when `currentTime > threshold` (default `3`); set to `0` to always skip to the previous track
|
|
1242
|
+
- `debugLabel?: string` — human-readable label shown in the devtools tab when multiple providers exist
|
|
1208
1243
|
|
|
1209
1244
|
### Queue mutation actions and single-track mode
|
|
1210
1245
|
|
|
@@ -1254,8 +1289,9 @@ Additional entrypoints:
|
|
|
1254
1289
|
- `@lucaismyname/ginger/remote`
|
|
1255
1290
|
- `@lucaismyname/ginger/crossfade`
|
|
1256
1291
|
- `@lucaismyname/ginger/experimental-gapless`
|
|
1292
|
+
- `@lucaismyname/ginger/devtools`
|
|
1257
1293
|
|
|
1258
|
-
See [Subpath Exports](#subpath-exports) for **`spatial`**, **`transcript`**, and **`
|
|
1294
|
+
See [Subpath Exports](#subpath-exports) for **`spatial`**, **`transcript`**, **`remote`**, and **`devtools`** usage. `experimental-gapless` is explicitly non-production and does not alter core playback.
|
|
1259
1295
|
|
|
1260
1296
|
## Notes
|
|
1261
1297
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useContext as n, useMemo as i, createContext as r } from "react";
|
|
2
|
+
const a = r(null), u = r(null), c = r(null), g = r(null);
|
|
3
|
+
function l() {
|
|
4
|
+
const e = n(a);
|
|
5
|
+
if (!e) throw new Error("Ginger hooks must be used within <Ginger.Provider>");
|
|
6
|
+
return e;
|
|
7
|
+
}
|
|
8
|
+
function _() {
|
|
9
|
+
const e = n(u);
|
|
10
|
+
if (!e) throw new Error("Ginger hooks must be used within <Ginger.Provider>");
|
|
11
|
+
return e;
|
|
12
|
+
}
|
|
13
|
+
function H() {
|
|
14
|
+
const e = n(c);
|
|
15
|
+
if (!e) throw new Error("Ginger hooks must be used within <Ginger.Provider>");
|
|
16
|
+
return e;
|
|
17
|
+
}
|
|
18
|
+
function I() {
|
|
19
|
+
const e = n(g);
|
|
20
|
+
if (!e) throw new Error("Ginger hooks must be used within <Ginger.Provider>");
|
|
21
|
+
return e;
|
|
22
|
+
}
|
|
23
|
+
function J() {
|
|
24
|
+
const e = l(), t = _();
|
|
25
|
+
return i(() => d(e, t), [e, t]);
|
|
26
|
+
}
|
|
27
|
+
function K(e, t) {
|
|
28
|
+
return { ...e, ...t };
|
|
29
|
+
}
|
|
30
|
+
function d(e, t) {
|
|
31
|
+
const {
|
|
32
|
+
init: m,
|
|
33
|
+
play: p,
|
|
34
|
+
pause: G,
|
|
35
|
+
togglePlayPause: f,
|
|
36
|
+
next: h,
|
|
37
|
+
prev: k,
|
|
38
|
+
setRepeatMode: x,
|
|
39
|
+
cycleRepeat: b,
|
|
40
|
+
toggleShuffle: w,
|
|
41
|
+
setQueue: y,
|
|
42
|
+
insertTrackAt: P,
|
|
43
|
+
removeTrackAt: C,
|
|
44
|
+
moveTrack: M,
|
|
45
|
+
enqueueNext: v,
|
|
46
|
+
playTrackAt: T,
|
|
47
|
+
selectTrackAt: R,
|
|
48
|
+
setPlaylistMeta: E,
|
|
49
|
+
setPlaybackMode: A,
|
|
50
|
+
dispatch: S,
|
|
51
|
+
...s
|
|
52
|
+
} = e, {
|
|
53
|
+
seek: q,
|
|
54
|
+
setVolume: F,
|
|
55
|
+
setMuted: V,
|
|
56
|
+
toggleMute: N,
|
|
57
|
+
setPlaybackRate: Q,
|
|
58
|
+
audioRef: j,
|
|
59
|
+
notifyEnded: z,
|
|
60
|
+
dispatch: B,
|
|
61
|
+
...o
|
|
62
|
+
} = t;
|
|
63
|
+
return { ...s, ...o };
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
a as G,
|
|
67
|
+
K as a,
|
|
68
|
+
I as b,
|
|
69
|
+
l as c,
|
|
70
|
+
J as d,
|
|
71
|
+
H as e,
|
|
72
|
+
c as f,
|
|
73
|
+
d as g,
|
|
74
|
+
g as h,
|
|
75
|
+
u as i,
|
|
76
|
+
_ as u
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=GingerSplitContexts-DQ3rESBu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GingerSplitContexts-DQ3rESBu.js","sources":["../src/context/GingerSplitContexts.tsx"],"sourcesContent":["import { type Dispatch, type MutableRefObject, createContext, useContext, useMemo } from \"react\";\nimport type {\n GingerAction,\n GingerInitPayload,\n GingerMediaControlSlice,\n GingerMediaSlice,\n GingerPlaybackSlice,\n GingerState,\n GingerTimeSlice,\n PlaybackMode,\n PlaylistMeta,\n RepeatMode,\n Track,\n} from \"../types\";\n\nexport type GingerPlaybackActions = {\n init: (payload: GingerInitPayload) => void;\n play: () => void;\n pause: () => void;\n togglePlayPause: () => void;\n next: () => void;\n prev: () => void;\n setRepeatMode: (mode: RepeatMode) => void;\n cycleRepeat: () => void;\n toggleShuffle: () => void;\n setQueue: (tracks: Track[], currentIndex?: number) => void;\n insertTrackAt: (track: Track, index?: number, autoPlay?: boolean) => void;\n removeTrackAt: (index: number) => void;\n moveTrack: (fromIndex: number, toIndex: number) => void;\n enqueueNext: (track: Track) => void;\n playTrackAt: (index: number) => void;\n selectTrackAt: (index: number) => void;\n setPlaylistMeta: (meta: PlaylistMeta | null) => void;\n setPlaybackMode: (mode: PlaybackMode) => void;\n dispatch: Dispatch<GingerAction>;\n};\n\nexport type GingerPlaybackContextValue = GingerPlaybackSlice & GingerPlaybackActions;\n\nexport type GingerMediaActions = {\n seek: (timeSeconds: number) => void;\n setVolume: (volume: number) => void;\n setMuted: (muted: boolean) => void;\n toggleMute: () => void;\n setPlaybackRate: (rate: number) => void;\n audioRef: MutableRefObject<HTMLAudioElement | null>;\n notifyEnded: () => void;\n dispatch: Dispatch<GingerAction>;\n};\n\nexport type GingerMediaContextValue = GingerMediaSlice & GingerMediaActions;\n\n/** High-frequency time/progress context value. */\nexport type GingerTimeContextValue = GingerTimeSlice;\n\n/** Low-frequency media control context value (volume, rate, actions). */\nexport type GingerMediaControlContextValue = GingerMediaControlSlice & GingerMediaActions;\n\nconst GingerPlaybackContext = createContext<GingerPlaybackContextValue | null>(null);\nconst GingerMediaContext = createContext<GingerMediaContextValue | null>(null);\nconst GingerTimeContext = createContext<GingerTimeContextValue | null>(null);\nconst GingerMediaControlContext = createContext<GingerMediaControlContextValue | null>(null);\n\nexport function useGingerPlayback(): GingerPlaybackContextValue {\n const ctx = useContext(GingerPlaybackContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/**\n * Full media context (time + controls merged). Re-renders on every time tick.\n * Prefer `useGingerTime()` or `useGingerMediaControls()` for narrower subscriptions.\n */\nexport function useGingerMedia(): GingerMediaContextValue {\n const ctx = useContext(GingerMediaContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/**\n * High-frequency time slice only (currentTime, duration, bufferedFraction, isBuffering, errorMessage).\n * Use when you only need progress/time data and want to avoid re-renders from volume/rate changes.\n */\nexport function useGingerTime(): GingerTimeContextValue {\n const ctx = useContext(GingerTimeContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/**\n * Low-frequency media controls (volume, muted, playbackRate, seek, setVolume, etc.).\n * Does NOT re-render on time ticks — only when volume, mute, or playback rate changes.\n */\nexport function useGingerMediaControls(): GingerMediaControlContextValue {\n const ctx = useContext(GingerMediaControlContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/** Full merged state; prefer over `useGingerContext().state` so updates follow playback vs media splits. */\nexport function useGingerState(): GingerState {\n const pb = useGingerPlayback();\n const md = useGingerMedia();\n return useMemo(() => gingerStateFromContextValues(pb, md), [pb, md]);\n}\n\n/** Merge playback + media slices (for selectors and `useGinger`). */\nexport function gingerStateFromContexts(\n playback: GingerPlaybackSlice,\n media: GingerMediaSlice,\n): GingerState {\n return { ...playback, ...media };\n}\n\n/** Merge full context values into `GingerState` (strips action fields). */\nexport function gingerStateFromContextValues(\n pb: GingerPlaybackContextValue,\n md: GingerMediaContextValue,\n): GingerState {\n const {\n init: _i,\n play: _p,\n pause: _pa,\n togglePlayPause: _t,\n next: _n,\n prev: _pr,\n setRepeatMode: _sr,\n cycleRepeat: _cr,\n toggleShuffle: _ts,\n setQueue: _sq,\n insertTrackAt: _ita,\n removeTrackAt: _rta,\n moveTrack: _mt,\n enqueueNext: _en,\n playTrackAt: _pta,\n selectTrackAt: _sta,\n setPlaylistMeta: _spm,\n setPlaybackMode: _spbm,\n dispatch: _d1,\n ...playbackRest\n } = pb;\n const {\n seek: _sk,\n setVolume: _sv,\n setMuted: _sm,\n toggleMute: _tm,\n setPlaybackRate: _spr,\n audioRef: _ar,\n notifyEnded: _ne,\n dispatch: _d2,\n ...mediaRest\n } = md;\n return { ...playbackRest, ...mediaRest };\n}\n\nexport { GingerPlaybackContext, GingerMediaContext, GingerTimeContext, GingerMediaControlContext };\n"],"names":["GingerPlaybackContext","createContext","GingerMediaContext","GingerTimeContext","GingerMediaControlContext","useGingerPlayback","ctx","useContext","useGingerMedia","useGingerTime","useGingerMediaControls","useGingerState","pb","md","useMemo","gingerStateFromContextValues","gingerStateFromContexts","playback","media","_i","_p","_pa","_t","_n","_pr","_sr","_cr","_ts","_sq","_ita","_rta","_mt","_en","_pta","_sta","_spm","_spbm","_d1","playbackRest","_sk","_sv","_sm","_tm","_spr","_ar","_ne","_d2","mediaRest"],"mappings":";AA0DA,MAAMA,IAAwBC,EAAiD,IAAI,GAC7EC,IAAqBD,EAA8C,IAAI,GACvEE,IAAoBF,EAA6C,IAAI,GACrEG,IAA4BH,EAAqD,IAAI;AAEpF,SAASI,IAAgD;AAC9D,QAAMC,IAAMC,EAAWP,CAAqB;AAC5C,MAAI,CAACM,EAAK,OAAM,IAAI,MAAM,oDAAoD;AAC9E,SAAOA;AACT;AAMO,SAASE,IAA0C;AACxD,QAAMF,IAAMC,EAAWL,CAAkB;AACzC,MAAI,CAACI,EAAK,OAAM,IAAI,MAAM,oDAAoD;AAC9E,SAAOA;AACT;AAMO,SAASG,IAAwC;AACtD,QAAMH,IAAMC,EAAWJ,CAAiB;AACxC,MAAI,CAACG,EAAK,OAAM,IAAI,MAAM,oDAAoD;AAC9E,SAAOA;AACT;AAMO,SAASI,IAAyD;AACvE,QAAMJ,IAAMC,EAAWH,CAAyB;AAChD,MAAI,CAACE,EAAK,OAAM,IAAI,MAAM,oDAAoD;AAC9E,SAAOA;AACT;AAGO,SAASK,IAA8B;AAC5C,QAAMC,IAAKP,EAAA,GACLQ,IAAKL,EAAA;AACX,SAAOM,EAAQ,MAAMC,EAA6BH,GAAIC,CAAE,GAAG,CAACD,GAAIC,CAAE,CAAC;AACrE;AAGO,SAASG,EACdC,GACAC,GACa;AACb,SAAO,EAAE,GAAGD,GAAU,GAAGC,EAAA;AAC3B;AAGO,SAASH,EACdH,GACAC,GACa;AACb,QAAM;AAAA,IACJ,MAAMM;AAAA,IACN,MAAMC;AAAA,IACN,OAAOC;AAAA,IACP,iBAAiBC;AAAA,IACjB,MAAMC;AAAA,IACN,MAAMC;AAAA,IACN,eAAeC;AAAA,IACf,aAAaC;AAAA,IACb,eAAeC;AAAA,IACf,UAAUC;AAAA,IACV,eAAeC;AAAA,IACf,eAAeC;AAAA,IACf,WAAWC;AAAA,IACX,aAAaC;AAAA,IACb,aAAaC;AAAA,IACb,eAAeC;AAAA,IACf,iBAAiBC;AAAA,IACjB,iBAAiBC;AAAA,IACjB,UAAUC;AAAA,IACV,GAAGC;AAAA,EAAA,IACD1B,GACE;AAAA,IACJ,MAAM2B;AAAA,IACN,WAAWC;AAAA,IACX,UAAUC;AAAA,IACV,YAAYC;AAAA,IACZ,iBAAiBC;AAAA,IACjB,UAAUC;AAAA,IACV,aAAaC;AAAA,IACb,UAAUC;AAAA,IACV,GAAGC;AAAA,EAAA,IACDlC;AACJ,SAAO,EAAE,GAAGyB,GAAc,GAAGS,EAAA;AAC/B;"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const t=require("react"),r=t.createContext(null),o=t.createContext(null),i=t.createContext(null),s=t.createContext(null);function a(){const e=t.useContext(r);if(!e)throw new Error("Ginger hooks must be used within <Ginger.Provider>");return e}function u(){const e=t.useContext(o);if(!e)throw new Error("Ginger hooks must be used within <Ginger.Provider>");return e}function d(){const e=t.useContext(i);if(!e)throw new Error("Ginger hooks must be used within <Ginger.Provider>");return e}function _(){const e=t.useContext(s);if(!e)throw new Error("Ginger hooks must be used within <Ginger.Provider>");return e}function x(){const e=a(),n=u();return t.useMemo(()=>c(e,n),[e,n])}function G(e,n){return{...e,...n}}function c(e,n){const{init:C,play:m,pause:k,togglePlayPause:p,next:f,prev:h,setRepeatMode:M,cycleRepeat:b,toggleShuffle:y,setQueue:P,insertTrackAt:w,removeTrackAt:T,moveTrack:v,enqueueNext:R,playTrackAt:S,selectTrackAt:E,setPlaylistMeta:A,setPlaybackMode:F,dispatch:q,...g}=e,{seek:V,setVolume:N,setMuted:Q,toggleMute:j,setPlaybackRate:z,audioRef:B,notifyEnded:D,dispatch:H,...l}=n;return{...g,...l}}exports.GingerMediaContext=o;exports.GingerMediaControlContext=s;exports.GingerPlaybackContext=r;exports.GingerTimeContext=i;exports.gingerStateFromContextValues=c;exports.gingerStateFromContexts=G;exports.useGingerMedia=u;exports.useGingerMediaControls=_;exports.useGingerPlayback=a;exports.useGingerState=x;exports.useGingerTime=d;
|
|
2
|
+
//# sourceMappingURL=GingerSplitContexts-KSB0vQ5F.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GingerSplitContexts-KSB0vQ5F.cjs","sources":["../src/context/GingerSplitContexts.tsx"],"sourcesContent":["import { type Dispatch, type MutableRefObject, createContext, useContext, useMemo } from \"react\";\nimport type {\n GingerAction,\n GingerInitPayload,\n GingerMediaControlSlice,\n GingerMediaSlice,\n GingerPlaybackSlice,\n GingerState,\n GingerTimeSlice,\n PlaybackMode,\n PlaylistMeta,\n RepeatMode,\n Track,\n} from \"../types\";\n\nexport type GingerPlaybackActions = {\n init: (payload: GingerInitPayload) => void;\n play: () => void;\n pause: () => void;\n togglePlayPause: () => void;\n next: () => void;\n prev: () => void;\n setRepeatMode: (mode: RepeatMode) => void;\n cycleRepeat: () => void;\n toggleShuffle: () => void;\n setQueue: (tracks: Track[], currentIndex?: number) => void;\n insertTrackAt: (track: Track, index?: number, autoPlay?: boolean) => void;\n removeTrackAt: (index: number) => void;\n moveTrack: (fromIndex: number, toIndex: number) => void;\n enqueueNext: (track: Track) => void;\n playTrackAt: (index: number) => void;\n selectTrackAt: (index: number) => void;\n setPlaylistMeta: (meta: PlaylistMeta | null) => void;\n setPlaybackMode: (mode: PlaybackMode) => void;\n dispatch: Dispatch<GingerAction>;\n};\n\nexport type GingerPlaybackContextValue = GingerPlaybackSlice & GingerPlaybackActions;\n\nexport type GingerMediaActions = {\n seek: (timeSeconds: number) => void;\n setVolume: (volume: number) => void;\n setMuted: (muted: boolean) => void;\n toggleMute: () => void;\n setPlaybackRate: (rate: number) => void;\n audioRef: MutableRefObject<HTMLAudioElement | null>;\n notifyEnded: () => void;\n dispatch: Dispatch<GingerAction>;\n};\n\nexport type GingerMediaContextValue = GingerMediaSlice & GingerMediaActions;\n\n/** High-frequency time/progress context value. */\nexport type GingerTimeContextValue = GingerTimeSlice;\n\n/** Low-frequency media control context value (volume, rate, actions). */\nexport type GingerMediaControlContextValue = GingerMediaControlSlice & GingerMediaActions;\n\nconst GingerPlaybackContext = createContext<GingerPlaybackContextValue | null>(null);\nconst GingerMediaContext = createContext<GingerMediaContextValue | null>(null);\nconst GingerTimeContext = createContext<GingerTimeContextValue | null>(null);\nconst GingerMediaControlContext = createContext<GingerMediaControlContextValue | null>(null);\n\nexport function useGingerPlayback(): GingerPlaybackContextValue {\n const ctx = useContext(GingerPlaybackContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/**\n * Full media context (time + controls merged). Re-renders on every time tick.\n * Prefer `useGingerTime()` or `useGingerMediaControls()` for narrower subscriptions.\n */\nexport function useGingerMedia(): GingerMediaContextValue {\n const ctx = useContext(GingerMediaContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/**\n * High-frequency time slice only (currentTime, duration, bufferedFraction, isBuffering, errorMessage).\n * Use when you only need progress/time data and want to avoid re-renders from volume/rate changes.\n */\nexport function useGingerTime(): GingerTimeContextValue {\n const ctx = useContext(GingerTimeContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/**\n * Low-frequency media controls (volume, muted, playbackRate, seek, setVolume, etc.).\n * Does NOT re-render on time ticks — only when volume, mute, or playback rate changes.\n */\nexport function useGingerMediaControls(): GingerMediaControlContextValue {\n const ctx = useContext(GingerMediaControlContext);\n if (!ctx) throw new Error(\"Ginger hooks must be used within <Ginger.Provider>\");\n return ctx;\n}\n\n/** Full merged state; prefer over `useGingerContext().state` so updates follow playback vs media splits. */\nexport function useGingerState(): GingerState {\n const pb = useGingerPlayback();\n const md = useGingerMedia();\n return useMemo(() => gingerStateFromContextValues(pb, md), [pb, md]);\n}\n\n/** Merge playback + media slices (for selectors and `useGinger`). */\nexport function gingerStateFromContexts(\n playback: GingerPlaybackSlice,\n media: GingerMediaSlice,\n): GingerState {\n return { ...playback, ...media };\n}\n\n/** Merge full context values into `GingerState` (strips action fields). */\nexport function gingerStateFromContextValues(\n pb: GingerPlaybackContextValue,\n md: GingerMediaContextValue,\n): GingerState {\n const {\n init: _i,\n play: _p,\n pause: _pa,\n togglePlayPause: _t,\n next: _n,\n prev: _pr,\n setRepeatMode: _sr,\n cycleRepeat: _cr,\n toggleShuffle: _ts,\n setQueue: _sq,\n insertTrackAt: _ita,\n removeTrackAt: _rta,\n moveTrack: _mt,\n enqueueNext: _en,\n playTrackAt: _pta,\n selectTrackAt: _sta,\n setPlaylistMeta: _spm,\n setPlaybackMode: _spbm,\n dispatch: _d1,\n ...playbackRest\n } = pb;\n const {\n seek: _sk,\n setVolume: _sv,\n setMuted: _sm,\n toggleMute: _tm,\n setPlaybackRate: _spr,\n audioRef: _ar,\n notifyEnded: _ne,\n dispatch: _d2,\n ...mediaRest\n } = md;\n return { ...playbackRest, ...mediaRest };\n}\n\nexport { GingerPlaybackContext, GingerMediaContext, GingerTimeContext, GingerMediaControlContext };\n"],"names":["GingerPlaybackContext","createContext","GingerMediaContext","GingerTimeContext","GingerMediaControlContext","useGingerPlayback","ctx","useContext","useGingerMedia","useGingerTime","useGingerMediaControls","useGingerState","pb","md","useMemo","gingerStateFromContextValues","gingerStateFromContexts","playback","media","_i","_p","_pa","_t","_n","_pr","_sr","_cr","_ts","_sq","_ita","_rta","_mt","_en","_pta","_sta","_spm","_spbm","_d1","playbackRest","_sk","_sv","_sm","_tm","_spr","_ar","_ne","_d2","mediaRest"],"mappings":"sCA0DMA,EAAwBC,EAAAA,cAAiD,IAAI,EAC7EC,EAAqBD,EAAAA,cAA8C,IAAI,EACvEE,EAAoBF,EAAAA,cAA6C,IAAI,EACrEG,EAA4BH,EAAAA,cAAqD,IAAI,EAEpF,SAASI,GAAgD,CAC9D,MAAMC,EAAMC,EAAAA,WAAWP,CAAqB,EAC5C,GAAI,CAACM,EAAK,MAAM,IAAI,MAAM,oDAAoD,EAC9E,OAAOA,CACT,CAMO,SAASE,GAA0C,CACxD,MAAMF,EAAMC,EAAAA,WAAWL,CAAkB,EACzC,GAAI,CAACI,EAAK,MAAM,IAAI,MAAM,oDAAoD,EAC9E,OAAOA,CACT,CAMO,SAASG,GAAwC,CACtD,MAAMH,EAAMC,EAAAA,WAAWJ,CAAiB,EACxC,GAAI,CAACG,EAAK,MAAM,IAAI,MAAM,oDAAoD,EAC9E,OAAOA,CACT,CAMO,SAASI,GAAyD,CACvE,MAAMJ,EAAMC,EAAAA,WAAWH,CAAyB,EAChD,GAAI,CAACE,EAAK,MAAM,IAAI,MAAM,oDAAoD,EAC9E,OAAOA,CACT,CAGO,SAASK,GAA8B,CAC5C,MAAMC,EAAKP,EAAA,EACLQ,EAAKL,EAAA,EACX,OAAOM,EAAAA,QAAQ,IAAMC,EAA6BH,EAAIC,CAAE,EAAG,CAACD,EAAIC,CAAE,CAAC,CACrE,CAGO,SAASG,EACdC,EACAC,EACa,CACb,MAAO,CAAE,GAAGD,EAAU,GAAGC,CAAA,CAC3B,CAGO,SAASH,EACdH,EACAC,EACa,CACb,KAAM,CACJ,KAAMM,EACN,KAAMC,EACN,MAAOC,EACP,gBAAiBC,EACjB,KAAMC,EACN,KAAMC,EACN,cAAeC,EACf,YAAaC,EACb,cAAeC,EACf,SAAUC,EACV,cAAeC,EACf,cAAeC,EACf,UAAWC,EACX,YAAaC,EACb,YAAaC,EACb,cAAeC,EACf,gBAAiBC,EACjB,gBAAiBC,EACjB,SAAUC,EACV,GAAGC,CAAA,EACD1B,EACE,CACJ,KAAM2B,EACN,UAAWC,EACX,SAAUC,EACV,WAAYC,EACZ,gBAAiBC,EACjB,SAAUC,EACV,YAAaC,EACb,SAAUC,EACV,GAAGC,CAAA,EACDlC,EACJ,MAAO,CAAE,GAAGyB,EAAc,GAAGS,CAAA,CAC/B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGingerLiveAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzer/useGingerLiveAnalyzer.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,4BAA4B,GAAG;IACzC,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,kHAAkH;IAClH,aAAa,EAAE,UAAU,CAAC;IAC1B,8DAA8D;IAC9D,cAAc,EAAE,UAAU,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAKF,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,
|
|
1
|
+
{"version":3,"file":"useGingerLiveAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzer/useGingerLiveAnalyzer.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,4BAA4B,GAAG;IACzC,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,kHAAkH;IAClH,aAAa,EAAE,UAAU,CAAC;IAC1B,8DAA8D;IAC9D,cAAc,EAAE,UAAU,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAKF,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,CAwK7B"}
|
package/dist/client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-B-W73LQC.cjs"),s=require("./useGinger-BMRLzjmr.cjs"),r=require("./useGingerChapterProgress-DLx-LayX.cjs"),a=require("./liveAudioGraph-0cpHD_Ic.cjs"),n=require("./selectors-CEGlYoFu.cjs"),i=require("./GingerSplitContexts-KSB0vQ5F.cjs");exports.Chapters=e.Chapters;exports.Ginger=e.Ginger;exports.LyricsSynced=e.LyricsSynced;exports.Pause=e.Pause;exports.Play=e.Play;exports.RepeatGlyph=e.RepeatGlyph;exports.ShuffleIcon=e.ShuffleIcon;exports.SkipBack=e.SkipBack;exports.SkipForward=e.SkipForward;exports.Volume2=e.Volume2;exports.VolumeX=e.VolumeX;exports.Wrapper=e.Wrapper;exports.clampPlaybackRate=e.clampPlaybackRate;exports.clampVolume=e.clampVolume;exports.defaultGingerLocale=e.defaultGingerLocale;exports.parseLrc=e.parseLrc;exports.useGingerChapters=e.useGingerChapters;exports.useGingerLocale=e.useGingerLocale;exports.useGingerLyricsSync=e.useGingerLyricsSync;exports.usePlayPauseBinding=e.usePlayPauseBinding;exports.useSeekBarBinding=e.useSeekBarBinding;exports.useVolumeSlider=e.useVolumeSlider;exports.useGinger=s.useGinger;exports.createGingerStore=r.createGingerStore;exports.useGingerChapterProgress=r.useGingerChapterProgress;exports.useGingerDebugLog=r.useGingerDebugLog;exports.useGingerKeyboardShortcuts=r.useGingerKeyboardShortcuts;exports.useGingerLiveAnalyzer=r.useGingerLiveAnalyzer;exports.useGingerPlaybackHistory=r.useGingerPlaybackHistory;exports.useGingerSleepTimer=r.useGingerSleepTimer;exports.useGingerVolumeFade=r.useGingerVolumeFade;exports.useNextTrackPrefetch=r.useNextTrackPrefetch;exports.useSeekDrag=r.useSeekDrag;exports.attachLiveAnalyser=a.attachLiveAnalyser;exports.detachLiveAnalyser=a.detachLiveAnalyser;exports.setProcessingChain=a.setProcessingChain;exports.derivePlaybackUiState=n.derivePlaybackUiState;exports.gingerStateFromContextValues=i.gingerStateFromContextValues;exports.gingerStateFromContexts=i.gingerStateFromContexts;exports.useGingerMedia=i.useGingerMedia;exports.useGingerMediaControls=i.useGingerMediaControls;exports.useGingerPlayback=i.useGingerPlayback;exports.useGingerState=i.useGingerState;exports.useGingerTime=i.useGingerTime;
|
|
2
2
|
//# sourceMappingURL=client.cjs.map
|
package/dist/client.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { C as s, G as r, L as i, P as
|
|
2
|
-
import { u as C } from "./useGinger-
|
|
3
|
-
import { c as v, u as B, a as F, b as A, d as R, e as
|
|
4
|
-
import { a as
|
|
1
|
+
import { C as s, G as r, L as i, P as u, a as n, R as t, S as o, b as g, c as l, V as c, d as G, W as p, e as d, f as m, g as y, p as S, u as f, h, i as P, j as k, k as L, l as b } from "./ginger-BHu7Ofna.js";
|
|
2
|
+
import { u as C } from "./useGinger-DKrHZ4NU.js";
|
|
3
|
+
import { c as v, u as B, a as F, b as A, d as R, e as T, f as D, g as M, h as W, i as j } from "./useGingerChapterProgress-DAIWjAtG.js";
|
|
4
|
+
import { a as z, d as H, s as I } from "./liveAudioGraph-DvPaxBCP.js";
|
|
5
5
|
import { d as N } from "./selectors-BT3WSsKN.js";
|
|
6
|
-
import { g as X, a as q, u as E, b as J, c as O } from "./GingerSplitContexts-
|
|
6
|
+
import { g as X, a as q, u as E, b as J, c as O, d as Q, e as Y } from "./GingerSplitContexts-DQ3rESBu.js";
|
|
7
7
|
export {
|
|
8
8
|
s as Chapters,
|
|
9
9
|
r as Ginger,
|
|
10
10
|
i as LyricsSynced,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
u as Pause,
|
|
12
|
+
n as Play,
|
|
13
|
+
t as RepeatGlyph,
|
|
14
14
|
o as ShuffleIcon,
|
|
15
15
|
g as SkipBack,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
l as SkipForward,
|
|
17
|
+
c as Volume2,
|
|
18
|
+
G as VolumeX,
|
|
19
|
+
p as Wrapper,
|
|
20
|
+
z as attachLiveAnalyser,
|
|
21
|
+
d as clampPlaybackRate,
|
|
22
|
+
m as clampVolume,
|
|
23
23
|
v as createGingerStore,
|
|
24
|
-
|
|
24
|
+
y as defaultGingerLocale,
|
|
25
25
|
N as derivePlaybackUiState,
|
|
26
|
-
|
|
26
|
+
H as detachLiveAnalyser,
|
|
27
27
|
X as gingerStateFromContextValues,
|
|
28
28
|
q as gingerStateFromContexts,
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
S as parseLrc,
|
|
30
|
+
I as setProcessingChain,
|
|
31
31
|
C as useGinger,
|
|
32
32
|
B as useGingerChapterProgress,
|
|
33
33
|
f as useGingerChapters,
|
|
@@ -37,15 +37,17 @@ export {
|
|
|
37
37
|
h as useGingerLocale,
|
|
38
38
|
P as useGingerLyricsSync,
|
|
39
39
|
E as useGingerMedia,
|
|
40
|
-
J as
|
|
41
|
-
|
|
42
|
-
T as
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
J as useGingerMediaControls,
|
|
41
|
+
O as useGingerPlayback,
|
|
42
|
+
T as useGingerPlaybackHistory,
|
|
43
|
+
D as useGingerSleepTimer,
|
|
44
|
+
Q as useGingerState,
|
|
45
|
+
Y as useGingerTime,
|
|
46
|
+
M as useGingerVolumeFade,
|
|
47
|
+
W as useNextTrackPrefetch,
|
|
46
48
|
k as usePlayPauseBinding,
|
|
47
49
|
L as useSeekBarBinding,
|
|
48
|
-
|
|
50
|
+
j as useSeekDrag,
|
|
49
51
|
b as useVolumeSlider
|
|
50
52
|
};
|
|
51
53
|
//# sourceMappingURL=client.js.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { GingerProviderProps } from '../types';
|
|
2
|
-
export declare function GingerProvider({ children, initialTracks, initialIndex, initialPlaylistMeta, initialShuffle, initialRepeatMode, initialPlaybackMode, initialPaused, initialVolume, initialMuted, initialPlaybackRate, initialStateKey, locale, mediaSession, beforePlay, onPlayBlocked, persistence, hydrateOnMount, resumeOnTrackChange, unstyled, asChild, className, style, dir: dirProp, prevRestartThresholdSeconds, onTrackChange, onPlay, onPause, onQueueEnd, onError, onVolumeChange, onPlaybackRateChange, onSeek, }: GingerProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function GingerProvider({ children, initialTracks, initialIndex, initialPlaylistMeta, initialShuffle, initialRepeatMode, initialPlaybackMode, initialPaused, initialVolume, initialMuted, initialPlaybackRate, initialStateKey, locale, mediaSession, beforePlay, onPlayBlocked, retryOnError, persistence, hydrateOnMount, resumeOnTrackChange, unstyled, asChild, className, style, dir: dirProp, prevRestartThresholdSeconds, onTrackChange, onPlay, onPause, onQueueEnd, onError, onVolumeChange, onPlaybackRateChange, onSeek, debugLabel, }: GingerProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=GingerProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GingerProvider.d.ts","sourceRoot":"","sources":["../../src/context/GingerProvider.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,mBAAmB,
|
|
1
|
+
{"version":3,"file":"GingerProvider.d.ts","sourceRoot":"","sources":["../../src/context/GingerProvider.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,mBAAmB,EAMpB,MAAM,UAAU,CAAC;AA2BlB,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,aAAkB,EAClB,YAAgB,EAChB,mBAA0B,EAC1B,cAAsB,EACtB,iBAAyB,EACzB,mBAAgC,EAChC,aAAoB,EACpB,aAAiB,EACjB,YAAoB,EACpB,mBAAuB,EACvB,eAAe,EACf,MAAM,EACN,YAAoB,EACpB,UAAU,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACX,cAAsB,EACtB,mBAA2B,EAC3B,QAAgB,EAChB,OAAe,EACf,SAAS,EACT,KAAK,EACL,GAAG,EAAE,OAAO,EACZ,2BAA+B,EAC/B,aAAa,EACb,MAAM,EACN,OAAO,EACP,UAAU,EACV,OAAO,EACP,cAAc,EACd,oBAAoB,EACpB,MAAM,EACN,UAAU,GACX,EAAE,mBAAmB,2CAqyBrB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Dispatch, MutableRefObject } from 'react';
|
|
2
|
-
import { GingerAction, GingerInitPayload, GingerMediaSlice, GingerPlaybackSlice, GingerState, PlaybackMode, PlaylistMeta, RepeatMode, Track } from '../types';
|
|
2
|
+
import { GingerAction, GingerInitPayload, GingerMediaControlSlice, GingerMediaSlice, GingerPlaybackSlice, GingerState, GingerTimeSlice, PlaybackMode, PlaylistMeta, RepeatMode, Track } from '../types';
|
|
3
3
|
export type GingerPlaybackActions = {
|
|
4
4
|
init: (payload: GingerInitPayload) => void;
|
|
5
5
|
play: () => void;
|
|
@@ -33,15 +33,35 @@ export type GingerMediaActions = {
|
|
|
33
33
|
dispatch: Dispatch<GingerAction>;
|
|
34
34
|
};
|
|
35
35
|
export type GingerMediaContextValue = GingerMediaSlice & GingerMediaActions;
|
|
36
|
+
/** High-frequency time/progress context value. */
|
|
37
|
+
export type GingerTimeContextValue = GingerTimeSlice;
|
|
38
|
+
/** Low-frequency media control context value (volume, rate, actions). */
|
|
39
|
+
export type GingerMediaControlContextValue = GingerMediaControlSlice & GingerMediaActions;
|
|
36
40
|
declare const GingerPlaybackContext: import('react').Context<GingerPlaybackContextValue | null>;
|
|
37
41
|
declare const GingerMediaContext: import('react').Context<GingerMediaContextValue | null>;
|
|
42
|
+
declare const GingerTimeContext: import('react').Context<GingerTimeSlice | null>;
|
|
43
|
+
declare const GingerMediaControlContext: import('react').Context<GingerMediaControlContextValue | null>;
|
|
38
44
|
export declare function useGingerPlayback(): GingerPlaybackContextValue;
|
|
45
|
+
/**
|
|
46
|
+
* Full media context (time + controls merged). Re-renders on every time tick.
|
|
47
|
+
* Prefer `useGingerTime()` or `useGingerMediaControls()` for narrower subscriptions.
|
|
48
|
+
*/
|
|
39
49
|
export declare function useGingerMedia(): GingerMediaContextValue;
|
|
50
|
+
/**
|
|
51
|
+
* High-frequency time slice only (currentTime, duration, bufferedFraction, isBuffering, errorMessage).
|
|
52
|
+
* Use when you only need progress/time data and want to avoid re-renders from volume/rate changes.
|
|
53
|
+
*/
|
|
54
|
+
export declare function useGingerTime(): GingerTimeContextValue;
|
|
55
|
+
/**
|
|
56
|
+
* Low-frequency media controls (volume, muted, playbackRate, seek, setVolume, etc.).
|
|
57
|
+
* Does NOT re-render on time ticks — only when volume, mute, or playback rate changes.
|
|
58
|
+
*/
|
|
59
|
+
export declare function useGingerMediaControls(): GingerMediaControlContextValue;
|
|
40
60
|
/** Full merged state; prefer over `useGingerContext().state` so updates follow playback vs media splits. */
|
|
41
61
|
export declare function useGingerState(): GingerState;
|
|
42
62
|
/** Merge playback + media slices (for selectors and `useGinger`). */
|
|
43
63
|
export declare function gingerStateFromContexts(playback: GingerPlaybackSlice, media: GingerMediaSlice): GingerState;
|
|
44
64
|
/** Merge full context values into `GingerState` (strips action fields). */
|
|
45
65
|
export declare function gingerStateFromContextValues(pb: GingerPlaybackContextValue, md: GingerMediaContextValue): GingerState;
|
|
46
|
-
export { GingerPlaybackContext, GingerMediaContext };
|
|
66
|
+
export { GingerPlaybackContext, GingerMediaContext, GingerTimeContext, GingerMediaControlContext };
|
|
47
67
|
//# sourceMappingURL=GingerSplitContexts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GingerSplitContexts.d.ts","sourceRoot":"","sources":["../../src/context/GingerSplitContexts.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,gBAAgB,EAAsC,MAAM,OAAO,CAAC;AACjG,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,KAAK,EACN,MAAM,UAAU,CAAC;AAElB,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC3C,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,aAAa,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IAC1C,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1E,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,eAAe,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,eAAe,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,mBAAmB,GAAG,qBAAqB,CAAC;AAErF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,gBAAgB,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACpD,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;AAE5E,QAAA,MAAM,qBAAqB,4DAAyD,CAAC;AACrF,QAAA,MAAM,kBAAkB,yDAAsD,CAAC;
|
|
1
|
+
{"version":3,"file":"GingerSplitContexts.d.ts","sourceRoot":"","sources":["../../src/context/GingerSplitContexts.tsx"],"names":[],"mappings":"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"}
|
package/dist/crossfade/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("react"),W=require("../GingerSplitContexts-KSB0vQ5F.cjs"),te=require("../transitions-Dx08t68T.cjs"),T=256;function ne(){const o=new Float32Array(T),i=new Float32Array(T);for(let n=0;n<T;n++){const t=n/(T-1);o[n]=Math.cos(t*(Math.PI/2)),i[n]=Math.sin(t*(Math.PI/2))}return{outCurve:o,inCurve:i}}function re(){if(!(typeof window>"u"))return window.AudioContext??window.webkitAudioContext}function z(o,i){const n=re();if(!n)throw new Error("[@lucaismyname/ginger/crossfade] Web Audio API is not available in this environment.");const t=new n,u=t.createMediaElementSource(o),d=t.createMediaElementSource(i),r=t.createGain(),s=t.createGain();return r.gain.value=1,s.gain.value=0,u.connect(r),r.connect(t.destination),d.connect(s),s.connect(t.destination),{context:t,outGain:r,inGain:s,outSource:u,inSource:d}}function j(o,i,n){const{context:t,outGain:u,inGain:d}=o,r=t.currentTime,s=r+i;if(n==="equal-power"){const{outCurve:p,inCurve:v}=ne();u.gain.setValueCurveAtTime(p,r,i),d.gain.setValueCurveAtTime(v,r,i)}else u.gain.setValueAtTime(1,r),u.gain.linearRampToValueAtTime(0,s),d.gain.setValueAtTime(0,r),d.gain.linearRampToValueAtTime(1,s)}function k(o){const i=[o.outSource,o.inSource,o.outGain,o.inGain];for(const n of i)try{n.disconnect()}catch{}o.context.close()}function oe(o={}){const{duration:i=3,curve:n="equal-power",crossOrigin:t,enabled:u=!0}=o,{tracks:d,currentIndex:r,isPaused:s,repeatMode:p,playbackMode:v,dispatch:R}=W.useGingerPlayback(),{currentTime:H,duration:Q,audioRef:P,muted:A,volume:C}=W.useGingerMedia(),[X,b]=c.useState(!1),[B,h]=c.useState(0),l=c.useRef(null),E=c.useCallback(()=>{const e=l.current;e&&(e.aborted=!0,clearTimeout(e.timeoutId),cancelAnimationFrame(e.rafId),k(e.graph),e.incomingAudio.pause(),e.incomingAudio.removeAttribute("src"),e.incomingAudio.load(),l.current=null,b(!1),h(0))},[]);c.useEffect(()=>{const e=l.current;e&&(s||r!==e.startedAtIndex)&&E()},[s,r,E]),c.useEffect(()=>()=>E(),[]),c.useEffect(()=>{const e=l.current;e&&(e.incomingAudio.volume=C,e.incomingAudio.muted=A)},[C,A]);const S=c.useRef({currentTime:0,trackDuration:0});S.current={currentTime:H,trackDuration:Q};const q=c.useRef({tracks:d,currentIndex:r,repeatMode:p,playbackMode:v,volume:C,muted:A});return q.current={tracks:d,currentIndex:r,repeatMode:p,playbackMode:v,volume:C,muted:A},c.useEffect(()=>{if(!u||s||typeof window>"u")return;const e=250;let f=null;const V=()=>{if(l.current)return;const{currentTime:J,trackDuration:_}=S.current;if(!(_>0))return;const w=_-J;if(w>i||w<=0)return;const{tracks:D,currentIndex:G,repeatMode:K,playbackMode:Y,volume:Z,muted:$}=q.current,I=te.computeEndedTransition({tracks:D,currentIndex:G,repeatMode:K,playbackMode:Y});if(I.kind==="stop")return;const F=I.kind==="replay_same"?G:I.nextIndex,y=D[F];if(!(y!=null&&y.fileUrl))return;const L=P.current;if(!L)return;const a=document.createElement("audio");a.preload="auto",a.volume=Z,a.muted=$,t&&(a.crossOrigin=t),a.src=y.fileUrl;let g;try{g=z(L,a)}catch(m){process.env.NODE_ENV!=="production"&&console.warn("[@lucaismyname/ginger/crossfade] Failed to attach crossfade graph. This may be because the audio element is already connected to a Web Audio graph (e.g. via useGingerEqualizer or useGingerLiveAnalyzer). These features are incompatible with useGingerCrossfade.",m);return}g.context.resume(),a.load(),a.play().catch(()=>{}),j(g,w,n);const O=performance.now(),x=w*1e3;b(!0),h(0);let M=0;const N=()=>{const m=performance.now()-O,U=Math.min(1,m/x);h(U),U<1&&(M=requestAnimationFrame(N))};M=requestAnimationFrame(N);const ee=setTimeout(()=>{const m=l.current;!m||m.aborted||(R({type:"SET_INDEX",payload:{index:F,autoPlay:!0}}),k(g),a.pause(),a.removeAttribute("src"),a.load(),l.current=null,b(!1),h(0))},x);l.current={graph:g,incomingAudio:a,startedAtIndex:G,startTime:O,fadeDurationMs:x,timeoutId:ee,rafId:M,aborted:!1},f!=null&&(clearInterval(f),f=null)};return f=setInterval(V,e),V(),()=>{f!=null&&clearInterval(f)}},[u,s,i,n,t,P,R]),{isCrossfading:X,crossfadeProgress:B}}exports.attachCrossfadeGraph=z;exports.scheduleCrossfade=j;exports.teardownCrossfadeGraph=k;exports.useGingerCrossfade=oe;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../src/crossfade/crossfadeGraph.ts","../../src/crossfade/useGingerCrossfade.ts"],"sourcesContent":["/**\n * Web Audio graph management for crossfade transitions.\n *\n * Creates a shared `AudioContext` that routes both the outgoing and incoming\n * `HTMLAudioElement` through individual `GainNode`s into the same destination.\n * Scheduling the gain ramps on both nodes produces the crossfade effect.\n *\n * **Compatibility note:** because the browser only permits one\n * `MediaElementAudioSourceNode` per `HTMLAudioElement`, this module is\n * incompatible with `liveAudioGraph`-based features (`useGingerEqualizer`,\n * `useGingerLiveAnalyzer`) on the same element. Using both simultaneously will\n * throw a `DOMException` when the second source node is requested.\n */\n\nexport type CrossfadeCurve = \"linear\" | \"equal-power\";\n\nexport type CrossfadeGraph = {\n context: AudioContext;\n outGain: GainNode;\n inGain: GainNode;\n outSource: MediaElementAudioSourceNode;\n inSource: MediaElementAudioSourceNode;\n};\n\nconst EQUAL_POWER_CURVE_LENGTH = 256;\n\nfunction buildEqualPowerCurves(): { outCurve: Float32Array; inCurve: Float32Array } {\n const outCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n const inCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n for (let i = 0; i < EQUAL_POWER_CURVE_LENGTH; i++) {\n const t = i / (EQUAL_POWER_CURVE_LENGTH - 1);\n outCurve[i] = Math.cos(t * (Math.PI / 2));\n inCurve[i] = Math.sin(t * (Math.PI / 2));\n }\n return { outCurve, inCurve };\n}\n\nfunction getAudioContextCtor(): (new (options?: AudioContextOptions) => AudioContext) | undefined {\n if (typeof window === \"undefined\") return undefined;\n return (\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext\n );\n}\n\n/**\n * Creates a shared `AudioContext` and connects both the outgoing and incoming\n * audio elements to it via individual `GainNode`s.\n *\n * The outgoing gain starts at 1, the incoming gain starts at 0.\n * Call `scheduleCrossfade` immediately after to begin the ramps.\n *\n * @throws `DOMException` if either element already has a `MediaElementAudioSourceNode`\n * in another context (e.g. created by `liveAudioGraph`).\n * @throws `Error` if the Web Audio API is unavailable in this environment.\n */\nexport function attachCrossfadeGraph(\n outgoing: HTMLAudioElement,\n incoming: HTMLAudioElement,\n): CrossfadeGraph {\n const Ctor = getAudioContextCtor();\n if (!Ctor) {\n throw new Error(\n \"[@lucaismyname/ginger/crossfade] Web Audio API is not available in this environment.\",\n );\n }\n\n const context = new Ctor();\n\n const outSource = context.createMediaElementSource(outgoing);\n const inSource = context.createMediaElementSource(incoming);\n\n const outGain = context.createGain();\n const inGain = context.createGain();\n\n outGain.gain.value = 1;\n inGain.gain.value = 0;\n\n outSource.connect(outGain);\n outGain.connect(context.destination);\n\n inSource.connect(inGain);\n inGain.connect(context.destination);\n\n return { context, outGain, inGain, outSource, inSource };\n}\n\n/**\n * Schedules gain ramps on both gain nodes so that `outGain` fades from 1 → 0\n * and `inGain` fades from 0 → 1 over `durationSec` seconds starting immediately.\n *\n * For `\"equal-power\"`, a cosine/sine curve is applied via `setValueCurveAtTime`\n * to maintain consistent perceived loudness throughout the transition.\n */\nexport function scheduleCrossfade(\n graph: CrossfadeGraph,\n durationSec: number,\n curve: CrossfadeCurve,\n): void {\n const { context, outGain, inGain } = graph;\n const startTime = context.currentTime;\n const endTime = startTime + durationSec;\n\n if (curve === \"equal-power\") {\n const { outCurve, inCurve } = buildEqualPowerCurves();\n outGain.gain.setValueCurveAtTime(outCurve, startTime, durationSec);\n inGain.gain.setValueCurveAtTime(inCurve, startTime, durationSec);\n } else {\n outGain.gain.setValueAtTime(1, startTime);\n outGain.gain.linearRampToValueAtTime(0, endTime);\n inGain.gain.setValueAtTime(0, startTime);\n inGain.gain.linearRampToValueAtTime(1, endTime);\n }\n}\n\n/**\n * Disconnects all nodes and closes the `AudioContext`.\n * Safe to call multiple times; errors during disconnect are silently ignored.\n */\nexport function teardownCrossfadeGraph(graph: CrossfadeGraph): void {\n const nodes: AudioNode[] = [graph.outSource, graph.inSource, graph.outGain, graph.inGain];\n for (const node of nodes) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n void graph.context.close();\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeEndedTransition } from \"../core/transitions\";\nimport {\n type CrossfadeCurve,\n type CrossfadeGraph,\n attachCrossfadeGraph,\n scheduleCrossfade,\n teardownCrossfadeGraph,\n} from \"./crossfadeGraph\";\n\nexport type { CrossfadeCurve };\n\nexport type UseGingerCrossfadeOptions = {\n /**\n * Duration of the crossfade in seconds.\n * The hook begins the fade when `timeRemaining ≤ duration`.\n * @default 3\n */\n duration?: number;\n /**\n * Gain curve shape applied to both gain nodes.\n * `\"equal-power\"` uses a cosine/sine curve to maintain consistent perceived\n * loudness; `\"linear\"` is a straight ramp that may dip slightly at the midpoint.\n * @default \"equal-power\"\n */\n curve?: CrossfadeCurve;\n /**\n * `crossOrigin` attribute for the incoming `<audio>` element.\n * Match this to the `crossOrigin` prop on `Ginger.Player` when serving\n * cross-origin audio so the browser can reuse the cached resource.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\";\n /**\n * When `false`, crossfade is completely disabled and playback falls back to\n * the default hard-cut transition.\n * @default true\n */\n enabled?: boolean;\n};\n\nexport type UseGingerCrossfadeResult = {\n /** `true` while a crossfade transition is actively in progress. */\n isCrossfading: boolean;\n /**\n * Progress of the current crossfade from `0` (start) to `1` (complete).\n * Always `0` when idle.\n */\n crossfadeProgress: number;\n};\n\ntype CrossfadeSession = {\n graph: CrossfadeGraph;\n incomingAudio: HTMLAudioElement;\n startedAtIndex: number;\n startTime: number;\n fadeDurationMs: number;\n timeoutId: ReturnType<typeof setTimeout>;\n rafId: number;\n aborted: boolean;\n};\n\n/**\n * Smoothly crossfades between consecutive tracks using the Web Audio API.\n *\n * When the remaining time on the current track falls below `duration`, the hook:\n * 1. Creates a hidden `<audio>` element and begins loading the next track.\n * 2. Routes both the outgoing and incoming elements through `GainNode`s in a\n * shared `AudioContext`.\n * 3. Schedules gain ramps so the outgoing track fades out while the incoming\n * track fades in simultaneously.\n * 4. Dispatches `SET_INDEX` once the ramp completes so the Ginger queue\n * advances to the new track.\n *\n * **Limitations:**\n * - Incompatible with `useGingerEqualizer` and `useGingerLiveAnalyzer` on the\n * same element — the browser only permits one `MediaElementAudioSourceNode`\n * per `<audio>` element.\n * - Requires a prior user gesture before `AudioContext` can be resumed (standard\n * Web Audio policy).\n * - When `repeatMode` is `\"one\"`, the crossfade replays the same track from the\n * beginning, matching the standard `notifyEnded` behaviour.\n *\n * Available as a subpath import:\n * ```ts\n * import { useGingerCrossfade } from \"@lucaismyname/ginger/crossfade\";\n * ```\n */\nexport function useGingerCrossfade(\n options: UseGingerCrossfadeOptions = {},\n): UseGingerCrossfadeResult {\n const { duration = 3, curve = \"equal-power\", crossOrigin, enabled = true } = options;\n\n const { tracks, currentIndex, isPaused, repeatMode, playbackMode, dispatch } =\n useGingerPlayback();\n const { currentTime, duration: trackDuration, audioRef, muted, volume } = useGingerMedia();\n\n const [isCrossfading, setIsCrossfading] = useState(false);\n const [crossfadeProgress, setCrossfadeProgress] = useState(0);\n\n const sessionRef = useRef<CrossfadeSession | null>(null);\n\n const abort = useCallback(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.aborted = true;\n clearTimeout(session.timeoutId);\n cancelAnimationFrame(session.rafId);\n teardownCrossfadeGraph(session.graph);\n session.incomingAudio.pause();\n session.incomingAudio.removeAttribute(\"src\");\n session.incomingAudio.load();\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, []);\n\n // Abort if the user pauses or manually advances/changes the track mid-fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n if (isPaused || currentIndex !== session.startedAtIndex) {\n abort();\n }\n }, [isPaused, currentIndex, abort]);\n\n // Clean up on unmount.\n // biome-ignore lint/correctness/useExhaustiveDependencies: abort is stable; intentional unmount-only cleanup\n useEffect(() => () => abort(), []);\n\n // Keep the incoming element's volume/muted in sync with Ginger state so that\n // the user's volume control applies to the incoming track during the fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.incomingAudio.volume = volume;\n session.incomingAudio.muted = muted;\n }, [volume, muted]);\n\n // Main trigger: start a crossfade when time remaining ≤ duration.\n useEffect(() => {\n if (!enabled) return;\n if (sessionRef.current) return; // already in progress\n if (isPaused) return;\n if (!(trackDuration > 0)) return;\n\n const timeRemaining = trackDuration - currentTime;\n if (timeRemaining > duration || timeRemaining <= 0) return;\n\n // Determine what comes next using the same logic as notifyEnded.\n const transition = computeEndedTransition({ tracks, currentIndex, repeatMode, playbackMode });\n if (transition.kind === \"stop\") return; // queue ends — nothing to crossfade into\n\n const nextIndex = transition.kind === \"replay_same\" ? currentIndex : transition.nextIndex;\n const nextTrack = tracks[nextIndex];\n if (!nextTrack?.fileUrl) return;\n\n const mainEl = audioRef.current;\n if (!mainEl) return;\n\n // Create the incoming element before attaching the graph so that both\n // elements are ready when createMediaElementSource is called.\n const incomingAudio = document.createElement(\"audio\");\n incomingAudio.preload = \"auto\";\n incomingAudio.volume = volume;\n incomingAudio.muted = muted;\n if (crossOrigin) incomingAudio.crossOrigin = crossOrigin;\n incomingAudio.src = nextTrack.fileUrl;\n\n let graph: CrossfadeGraph;\n try {\n graph = attachCrossfadeGraph(mainEl, incomingAudio);\n } catch (e) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"[@lucaismyname/ginger/crossfade] Failed to attach crossfade graph. \" +\n \"This may be because the audio element is already connected to a Web Audio graph \" +\n \"(e.g. via useGingerEqualizer or useGingerLiveAnalyzer). \" +\n \"These features are incompatible with useGingerCrossfade.\",\n e,\n );\n }\n return;\n }\n\n // Browsers suspend AudioContext until a user gesture has occurred.\n void graph.context.resume();\n\n incomingAudio.load();\n void incomingAudio.play().catch(() => {\n // Autoplay may be blocked; the gain ramps continue regardless and the\n // incoming audio will play once the browser permits it.\n });\n\n scheduleCrossfade(graph, timeRemaining, curve);\n\n const startTime = performance.now();\n const fadeDurationMs = timeRemaining * 1000;\n\n setIsCrossfading(true);\n setCrossfadeProgress(0);\n\n // rAF loop: drive crossfadeProgress for consumers (visualisers, UI indicators).\n let rafId = 0;\n const tick = () => {\n const elapsed = performance.now() - startTime;\n const progress = Math.min(1, elapsed / fadeDurationMs);\n setCrossfadeProgress(progress);\n if (progress < 1) {\n rafId = requestAnimationFrame(tick);\n }\n };\n rafId = requestAnimationFrame(tick);\n\n // Once the gain ramps complete, advance the Ginger queue.\n const timeoutId = setTimeout(() => {\n const session = sessionRef.current;\n if (!session || session.aborted) return;\n\n // GingerPlayer will swap the src on the main element and resume playback.\n dispatch({ type: \"SET_INDEX\", payload: { index: nextIndex, autoPlay: true } });\n\n teardownCrossfadeGraph(graph);\n incomingAudio.pause();\n incomingAudio.removeAttribute(\"src\");\n incomingAudio.load();\n\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, fadeDurationMs);\n\n sessionRef.current = {\n graph,\n incomingAudio,\n startedAtIndex: currentIndex,\n startTime,\n fadeDurationMs,\n timeoutId,\n rafId,\n aborted: false,\n };\n }, [\n enabled,\n isPaused,\n trackDuration,\n currentTime,\n duration,\n curve,\n crossOrigin,\n tracks,\n currentIndex,\n repeatMode,\n playbackMode,\n audioRef,\n volume,\n muted,\n dispatch,\n ]);\n\n return { isCrossfading, crossfadeProgress };\n}\n"],"names":["EQUAL_POWER_CURVE_LENGTH","buildEqualPowerCurves","outCurve","inCurve","i","t","getAudioContextCtor","attachCrossfadeGraph","outgoing","incoming","Ctor","context","outSource","inSource","outGain","inGain","scheduleCrossfade","graph","durationSec","curve","startTime","endTime","teardownCrossfadeGraph","nodes","node","useGingerCrossfade","options","duration","crossOrigin","enabled","tracks","currentIndex","isPaused","repeatMode","playbackMode","dispatch","useGingerPlayback","currentTime","trackDuration","audioRef","muted","volume","useGingerMedia","isCrossfading","setIsCrossfading","useState","crossfadeProgress","setCrossfadeProgress","sessionRef","useRef","abort","useCallback","session","useEffect","timeRemaining","transition","computeEndedTransition","nextIndex","nextTrack","mainEl","incomingAudio","e","fadeDurationMs","rafId","tick","elapsed","progress","timeoutId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/crossfade/crossfadeGraph.ts","../../src/crossfade/useGingerCrossfade.ts"],"sourcesContent":["/**\n * Web Audio graph management for crossfade transitions.\n *\n * Creates a shared `AudioContext` that routes both the outgoing and incoming\n * `HTMLAudioElement` through individual `GainNode`s into the same destination.\n * Scheduling the gain ramps on both nodes produces the crossfade effect.\n *\n * **Compatibility note:** because the browser only permits one\n * `MediaElementAudioSourceNode` per `HTMLAudioElement`, this module is\n * incompatible with `liveAudioGraph`-based features (`useGingerEqualizer`,\n * `useGingerLiveAnalyzer`) on the same element. Using both simultaneously will\n * throw a `DOMException` when the second source node is requested.\n */\n\nexport type CrossfadeCurve = \"linear\" | \"equal-power\";\n\nexport type CrossfadeGraph = {\n context: AudioContext;\n outGain: GainNode;\n inGain: GainNode;\n outSource: MediaElementAudioSourceNode;\n inSource: MediaElementAudioSourceNode;\n};\n\nconst EQUAL_POWER_CURVE_LENGTH = 256;\n\nfunction buildEqualPowerCurves(): { outCurve: Float32Array; inCurve: Float32Array } {\n const outCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n const inCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n for (let i = 0; i < EQUAL_POWER_CURVE_LENGTH; i++) {\n const t = i / (EQUAL_POWER_CURVE_LENGTH - 1);\n outCurve[i] = Math.cos(t * (Math.PI / 2));\n inCurve[i] = Math.sin(t * (Math.PI / 2));\n }\n return { outCurve, inCurve };\n}\n\nfunction getAudioContextCtor(): (new (options?: AudioContextOptions) => AudioContext) | undefined {\n if (typeof window === \"undefined\") return undefined;\n return (\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext\n );\n}\n\n/**\n * Creates a shared `AudioContext` and connects both the outgoing and incoming\n * audio elements to it via individual `GainNode`s.\n *\n * The outgoing gain starts at 1, the incoming gain starts at 0.\n * Call `scheduleCrossfade` immediately after to begin the ramps.\n *\n * @throws `DOMException` if either element already has a `MediaElementAudioSourceNode`\n * in another context (e.g. created by `liveAudioGraph`).\n * @throws `Error` if the Web Audio API is unavailable in this environment.\n */\nexport function attachCrossfadeGraph(\n outgoing: HTMLAudioElement,\n incoming: HTMLAudioElement,\n): CrossfadeGraph {\n const Ctor = getAudioContextCtor();\n if (!Ctor) {\n throw new Error(\n \"[@lucaismyname/ginger/crossfade] Web Audio API is not available in this environment.\",\n );\n }\n\n const context = new Ctor();\n\n const outSource = context.createMediaElementSource(outgoing);\n const inSource = context.createMediaElementSource(incoming);\n\n const outGain = context.createGain();\n const inGain = context.createGain();\n\n outGain.gain.value = 1;\n inGain.gain.value = 0;\n\n outSource.connect(outGain);\n outGain.connect(context.destination);\n\n inSource.connect(inGain);\n inGain.connect(context.destination);\n\n return { context, outGain, inGain, outSource, inSource };\n}\n\n/**\n * Schedules gain ramps on both gain nodes so that `outGain` fades from 1 → 0\n * and `inGain` fades from 0 → 1 over `durationSec` seconds starting immediately.\n *\n * For `\"equal-power\"`, a cosine/sine curve is applied via `setValueCurveAtTime`\n * to maintain consistent perceived loudness throughout the transition.\n */\nexport function scheduleCrossfade(\n graph: CrossfadeGraph,\n durationSec: number,\n curve: CrossfadeCurve,\n): void {\n const { context, outGain, inGain } = graph;\n const startTime = context.currentTime;\n const endTime = startTime + durationSec;\n\n if (curve === \"equal-power\") {\n const { outCurve, inCurve } = buildEqualPowerCurves();\n outGain.gain.setValueCurveAtTime(outCurve, startTime, durationSec);\n inGain.gain.setValueCurveAtTime(inCurve, startTime, durationSec);\n } else {\n outGain.gain.setValueAtTime(1, startTime);\n outGain.gain.linearRampToValueAtTime(0, endTime);\n inGain.gain.setValueAtTime(0, startTime);\n inGain.gain.linearRampToValueAtTime(1, endTime);\n }\n}\n\n/**\n * Disconnects all nodes and closes the `AudioContext`.\n * Safe to call multiple times; errors during disconnect are silently ignored.\n */\nexport function teardownCrossfadeGraph(graph: CrossfadeGraph): void {\n const nodes: AudioNode[] = [graph.outSource, graph.inSource, graph.outGain, graph.inGain];\n for (const node of nodes) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n void graph.context.close();\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeEndedTransition } from \"../core/transitions\";\nimport {\n type CrossfadeCurve,\n type CrossfadeGraph,\n attachCrossfadeGraph,\n scheduleCrossfade,\n teardownCrossfadeGraph,\n} from \"./crossfadeGraph\";\n\nexport type { CrossfadeCurve };\n\nexport type UseGingerCrossfadeOptions = {\n /**\n * Duration of the crossfade in seconds.\n * The hook begins the fade when `timeRemaining ≤ duration`.\n * @default 3\n */\n duration?: number;\n /**\n * Gain curve shape applied to both gain nodes.\n * `\"equal-power\"` uses a cosine/sine curve to maintain consistent perceived\n * loudness; `\"linear\"` is a straight ramp that may dip slightly at the midpoint.\n * @default \"equal-power\"\n */\n curve?: CrossfadeCurve;\n /**\n * `crossOrigin` attribute for the incoming `<audio>` element.\n * Match this to the `crossOrigin` prop on `Ginger.Player` when serving\n * cross-origin audio so the browser can reuse the cached resource.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\";\n /**\n * When `false`, crossfade is completely disabled and playback falls back to\n * the default hard-cut transition.\n * @default true\n */\n enabled?: boolean;\n};\n\nexport type UseGingerCrossfadeResult = {\n /** `true` while a crossfade transition is actively in progress. */\n isCrossfading: boolean;\n /**\n * Progress of the current crossfade from `0` (start) to `1` (complete).\n * Always `0` when idle.\n */\n crossfadeProgress: number;\n};\n\ntype CrossfadeSession = {\n graph: CrossfadeGraph;\n incomingAudio: HTMLAudioElement;\n startedAtIndex: number;\n startTime: number;\n fadeDurationMs: number;\n timeoutId: ReturnType<typeof setTimeout>;\n rafId: number;\n aborted: boolean;\n};\n\n/**\n * Smoothly crossfades between consecutive tracks using the Web Audio API.\n *\n * When the remaining time on the current track falls below `duration`, the hook:\n * 1. Creates a hidden `<audio>` element and begins loading the next track.\n * 2. Routes both the outgoing and incoming elements through `GainNode`s in a\n * shared `AudioContext`.\n * 3. Schedules gain ramps so the outgoing track fades out while the incoming\n * track fades in simultaneously.\n * 4. Dispatches `SET_INDEX` once the ramp completes so the Ginger queue\n * advances to the new track.\n *\n * **Limitations:**\n * - Incompatible with `useGingerEqualizer` and `useGingerLiveAnalyzer` on the\n * same element — the browser only permits one `MediaElementAudioSourceNode`\n * per `<audio>` element.\n * - Requires a prior user gesture before `AudioContext` can be resumed (standard\n * Web Audio policy).\n * - When `repeatMode` is `\"one\"`, the crossfade replays the same track from the\n * beginning, matching the standard `notifyEnded` behaviour.\n *\n * Available as a subpath import:\n * ```ts\n * import { useGingerCrossfade } from \"@lucaismyname/ginger/crossfade\";\n * ```\n */\nexport function useGingerCrossfade(\n options: UseGingerCrossfadeOptions = {},\n): UseGingerCrossfadeResult {\n const { duration = 3, curve = \"equal-power\", crossOrigin, enabled = true } = options;\n\n const { tracks, currentIndex, isPaused, repeatMode, playbackMode, dispatch } =\n useGingerPlayback();\n const { currentTime, duration: trackDuration, audioRef, muted, volume } = useGingerMedia();\n\n const [isCrossfading, setIsCrossfading] = useState(false);\n const [crossfadeProgress, setCrossfadeProgress] = useState(0);\n\n const sessionRef = useRef<CrossfadeSession | null>(null);\n\n const abort = useCallback(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.aborted = true;\n clearTimeout(session.timeoutId);\n cancelAnimationFrame(session.rafId);\n teardownCrossfadeGraph(session.graph);\n session.incomingAudio.pause();\n session.incomingAudio.removeAttribute(\"src\");\n session.incomingAudio.load();\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, []);\n\n // Abort if the user pauses or manually advances/changes the track mid-fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n if (isPaused || currentIndex !== session.startedAtIndex) {\n abort();\n }\n }, [isPaused, currentIndex, abort]);\n\n // Clean up on unmount.\n // biome-ignore lint/correctness/useExhaustiveDependencies: abort is stable; intentional unmount-only cleanup\n useEffect(() => () => abort(), []);\n\n // Keep the incoming element's volume/muted in sync with Ginger state so that\n // the user's volume control applies to the incoming track during the fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.incomingAudio.volume = volume;\n session.incomingAudio.muted = muted;\n }, [volume, muted]);\n\n // Refs for polling-based crossfade trigger (avoids re-running the effect on every time tick).\n const mediaRef = useRef({ currentTime: 0, trackDuration: 0 });\n mediaRef.current = { currentTime, trackDuration };\n\n const crossfadeConfigRef = useRef({\n tracks,\n currentIndex,\n repeatMode,\n playbackMode,\n volume,\n muted,\n });\n crossfadeConfigRef.current = { tracks, currentIndex, repeatMode, playbackMode, volume, muted };\n\n // Main trigger: poll at a reasonable interval to detect when crossfade should start.\n useEffect(() => {\n if (!enabled || isPaused || typeof window === \"undefined\") return;\n\n const POLL_INTERVAL_MS = 250;\n let pollId: ReturnType<typeof setInterval> | null = null;\n\n const tryStartCrossfade = () => {\n if (sessionRef.current) return;\n\n const { currentTime: ct, trackDuration: td } = mediaRef.current;\n if (!(td > 0)) return;\n\n const timeRemaining = td - ct;\n if (timeRemaining > duration || timeRemaining <= 0) return;\n\n const {\n tracks: tr,\n currentIndex: ci,\n repeatMode: rm,\n playbackMode: pm,\n volume: v,\n muted: m,\n } = crossfadeConfigRef.current;\n\n const transition = computeEndedTransition({\n tracks: tr,\n currentIndex: ci,\n repeatMode: rm,\n playbackMode: pm,\n });\n if (transition.kind === \"stop\") return;\n\n const nextIndex = transition.kind === \"replay_same\" ? ci : transition.nextIndex;\n const nextTrack = tr[nextIndex];\n if (!nextTrack?.fileUrl) return;\n\n const mainEl = audioRef.current;\n if (!mainEl) return;\n\n const incomingAudio = document.createElement(\"audio\");\n incomingAudio.preload = \"auto\";\n incomingAudio.volume = v;\n incomingAudio.muted = m;\n if (crossOrigin) incomingAudio.crossOrigin = crossOrigin;\n incomingAudio.src = nextTrack.fileUrl;\n\n let graph: CrossfadeGraph;\n try {\n graph = attachCrossfadeGraph(mainEl, incomingAudio);\n } catch (e) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"[@lucaismyname/ginger/crossfade] Failed to attach crossfade graph. \" +\n \"This may be because the audio element is already connected to a Web Audio graph \" +\n \"(e.g. via useGingerEqualizer or useGingerLiveAnalyzer). \" +\n \"These features are incompatible with useGingerCrossfade.\",\n e,\n );\n }\n return;\n }\n\n void graph.context.resume();\n\n incomingAudio.load();\n void incomingAudio.play().catch(() => {\n // Autoplay may be blocked; gain ramps continue regardless.\n });\n\n scheduleCrossfade(graph, timeRemaining, curve);\n\n const startTime = performance.now();\n const fadeDurationMs = timeRemaining * 1000;\n\n setIsCrossfading(true);\n setCrossfadeProgress(0);\n\n let rafId = 0;\n const tick = () => {\n const elapsed = performance.now() - startTime;\n const progress = Math.min(1, elapsed / fadeDurationMs);\n setCrossfadeProgress(progress);\n if (progress < 1) {\n rafId = requestAnimationFrame(tick);\n }\n };\n rafId = requestAnimationFrame(tick);\n\n const timeoutId = setTimeout(() => {\n const session = sessionRef.current;\n if (!session || session.aborted) return;\n\n dispatch({ type: \"SET_INDEX\", payload: { index: nextIndex, autoPlay: true } });\n\n teardownCrossfadeGraph(graph);\n incomingAudio.pause();\n incomingAudio.removeAttribute(\"src\");\n incomingAudio.load();\n\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, fadeDurationMs);\n\n sessionRef.current = {\n graph,\n incomingAudio,\n startedAtIndex: ci,\n startTime,\n fadeDurationMs,\n timeoutId,\n rafId,\n aborted: false,\n };\n\n if (pollId != null) {\n clearInterval(pollId);\n pollId = null;\n }\n };\n\n pollId = setInterval(tryStartCrossfade, POLL_INTERVAL_MS);\n tryStartCrossfade();\n\n return () => {\n if (pollId != null) clearInterval(pollId);\n };\n }, [enabled, isPaused, duration, curve, crossOrigin, audioRef, dispatch]);\n\n return { isCrossfading, crossfadeProgress };\n}\n"],"names":["EQUAL_POWER_CURVE_LENGTH","buildEqualPowerCurves","outCurve","inCurve","i","getAudioContextCtor","attachCrossfadeGraph","outgoing","incoming","Ctor","context","outSource","inSource","outGain","inGain","scheduleCrossfade","graph","durationSec","curve","startTime","endTime","teardownCrossfadeGraph","nodes","node","useGingerCrossfade","options","duration","crossOrigin","enabled","tracks","currentIndex","isPaused","repeatMode","playbackMode","dispatch","useGingerPlayback","currentTime","trackDuration","audioRef","muted","volume","useGingerMedia","isCrossfading","setIsCrossfading","useState","crossfadeProgress","setCrossfadeProgress","sessionRef","useRef","abort","useCallback","session","useEffect","mediaRef","crossfadeConfigRef","POLL_INTERVAL_MS","pollId","tryStartCrossfade","ct","td","timeRemaining","tr","ci","rm","pm","v","m","transition","computeEndedTransition","nextIndex","nextTrack","mainEl","incomingAudio","e","fadeDurationMs","rafId","tick","elapsed","progress","timeoutId"],"mappings":"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"}
|