@lucaismyname/ginger 0.0.60 → 0.0.61
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 +272 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,8 @@ For docs beyond this README, use the repository links below:
|
|
|
69
69
|
|
|
70
70
|
## Subpath Exports
|
|
71
71
|
|
|
72
|
+
Optional entrypoints keep the core bundle small. **Copy-paste starters** (full `Ginger.Provider` + `Ginger.Player` + subpath wiring) are in [Subpath copy-paste starters](#subpath-copy-paste-starters) below.
|
|
73
|
+
|
|
72
74
|
- `@lucaismyname/ginger/client`
|
|
73
75
|
- `@lucaismyname/ginger/testing`
|
|
74
76
|
- `@lucaismyname/ginger/waveform`
|
|
@@ -81,12 +83,101 @@ For docs beyond this README, use the repository links below:
|
|
|
81
83
|
- `@lucaismyname/ginger/experimental-gapless`
|
|
82
84
|
- `@lucaismyname/ginger/devtools`
|
|
83
85
|
|
|
84
|
-
###
|
|
86
|
+
### Subpath Examples
|
|
87
|
+
|
|
88
|
+
Each snippet is a **single-file starting point**: replace `/your-audio.mp3` (or any `fileUrl`) with a real URL, then layer your UI. Imports use the published package names.
|
|
89
|
+
|
|
90
|
+
#### <a id="subpath-starter-client"></a> `@lucaismyname/ginger/client`
|
|
91
|
+
|
|
92
|
+
Same API as the root package, with a `"use client"` directive for React Server Components (for example Next.js App Router).
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
"use client";
|
|
96
|
+
|
|
97
|
+
import { Ginger } from "@lucaismyname/ginger/client";
|
|
98
|
+
|
|
99
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
100
|
+
|
|
101
|
+
export function App() {
|
|
102
|
+
return (
|
|
103
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
104
|
+
<Ginger.Player />
|
|
105
|
+
<Ginger.Control.PlayPause />
|
|
106
|
+
</Ginger.Provider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### <a id="subpath-starter-testing"></a> `@lucaismyname/ginger/testing`
|
|
112
|
+
|
|
113
|
+
Vitest (or Jest) example: `renderGinger` wraps **`Ginger.Provider`** and optionally **`Ginger.Player`**.
|
|
85
114
|
|
|
86
115
|
```tsx
|
|
116
|
+
import type { Track } from "@lucaismyname/ginger";
|
|
117
|
+
import { queryAudio, renderGinger } from "@lucaismyname/ginger/testing";
|
|
118
|
+
import { describe, expect, it } from "vitest";
|
|
119
|
+
|
|
120
|
+
const tracks: Track[] = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
121
|
+
|
|
122
|
+
describe("Ginger", () => {
|
|
123
|
+
it("mounts audio", () => {
|
|
124
|
+
const { container } = renderGinger(<p>ok</p>, { tracks });
|
|
125
|
+
expect(queryAudio(container)).toBeTruthy();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### <a id="subpath-starter-waveform"></a> `@lucaismyname/ginger/waveform`
|
|
131
|
+
|
|
132
|
+
`useAudioPeaks` reads the **same** URL as the current track (offline scan; keep files reasonably small).
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { Ginger, useGinger } from "@lucaismyname/ginger";
|
|
136
|
+
import { useAudioPeaks } from "@lucaismyname/ginger/waveform";
|
|
137
|
+
|
|
138
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
139
|
+
|
|
140
|
+
function PeaksRow() {
|
|
141
|
+
const { currentTrack } = useGinger();
|
|
142
|
+
const { peaks, isLoading, error } = useAudioPeaks(currentTrack?.fileUrl, 32);
|
|
143
|
+
if (error) return <p>{error}</p>;
|
|
144
|
+
if (isLoading) return <p>Scanning…</p>;
|
|
145
|
+
return (
|
|
146
|
+
<div style={{ display: "flex", gap: 1, height: 24, alignItems: "flex-end" }}>
|
|
147
|
+
{peaks.map((p, i) => (
|
|
148
|
+
<span
|
|
149
|
+
key={i}
|
|
150
|
+
style={{
|
|
151
|
+
width: 3,
|
|
152
|
+
height: `${Math.max(2, p * 24)}px`,
|
|
153
|
+
background: "#ea580c",
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function App() {
|
|
162
|
+
return (
|
|
163
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
164
|
+
<Ginger.Player />
|
|
165
|
+
<Ginger.Control.PlayPause />
|
|
166
|
+
<PeaksRow />
|
|
167
|
+
</Ginger.Provider>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### <a id="subpath-starter-equalizer"></a> `@lucaismyname/ginger/equalizer`
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { Ginger } from "@lucaismyname/ginger";
|
|
87
176
|
import { useGingerEqualizer } from "@lucaismyname/ginger/equalizer";
|
|
88
177
|
|
|
89
|
-
|
|
178
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
179
|
+
|
|
180
|
+
function EqSliders() {
|
|
90
181
|
const { setBandGain, bands, error } = useGingerEqualizer({
|
|
91
182
|
bands: [
|
|
92
183
|
{ frequency: 60 },
|
|
@@ -96,7 +187,6 @@ function MyPlayer() {
|
|
|
96
187
|
{ frequency: 16000 },
|
|
97
188
|
],
|
|
98
189
|
});
|
|
99
|
-
|
|
100
190
|
return (
|
|
101
191
|
<div>
|
|
102
192
|
{bands.map((band, i) => (
|
|
@@ -115,24 +205,32 @@ function MyPlayer() {
|
|
|
115
205
|
</div>
|
|
116
206
|
);
|
|
117
207
|
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
The EQ and `useGingerLiveAnalyzer` share the same `AudioContext` and can be used together. EQ filters are inserted before the analyser in the Web Audio graph.
|
|
121
208
|
|
|
122
|
-
|
|
209
|
+
export function App() {
|
|
210
|
+
return (
|
|
211
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
212
|
+
<Ginger.Player />
|
|
213
|
+
<Ginger.Control.PlayPause />
|
|
214
|
+
<EqSliders />
|
|
215
|
+
</Ginger.Provider>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
123
219
|
|
|
124
|
-
|
|
220
|
+
#### <a id="subpath-starter-spatial"></a> `@lucaismyname/ginger/spatial`
|
|
125
221
|
|
|
126
222
|
```tsx
|
|
223
|
+
import { Ginger } from "@lucaismyname/ginger";
|
|
127
224
|
import { useGingerSpatialAudio } from "@lucaismyname/ginger/spatial";
|
|
128
225
|
|
|
129
|
-
|
|
226
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
227
|
+
|
|
228
|
+
function SpatialControls() {
|
|
130
229
|
const { setSourcePosition, error } = useGingerSpatialAudio({
|
|
131
230
|
panningModel: "HRTF",
|
|
132
231
|
position: [2, 0, 0],
|
|
133
232
|
listenerPosition: [0, 0, 0],
|
|
134
233
|
});
|
|
135
|
-
|
|
136
234
|
return (
|
|
137
235
|
<div>
|
|
138
236
|
<button type="button" onClick={() => setSourcePosition(0, 0, -2)}>
|
|
@@ -142,20 +240,25 @@ function Spatialized() {
|
|
|
142
240
|
</div>
|
|
143
241
|
);
|
|
144
242
|
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Use **`setListenerPosition`** and **`setPanningModel`** for runtime updates without rebuilding the graph.
|
|
148
243
|
|
|
149
|
-
|
|
244
|
+
export function App() {
|
|
245
|
+
return (
|
|
246
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
247
|
+
<Ginger.Player />
|
|
248
|
+
<Ginger.Control.PlayPause />
|
|
249
|
+
<SpatialControls />
|
|
250
|
+
</Ginger.Provider>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
150
254
|
|
|
151
|
-
|
|
255
|
+
#### <a id="subpath-starter-transcript"></a> `@lucaismyname/ginger/transcript`
|
|
152
256
|
|
|
153
257
|
```tsx
|
|
154
|
-
import {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} from "@lucaismyname/ginger/transcript";
|
|
258
|
+
import { Ginger } from "@lucaismyname/ginger";
|
|
259
|
+
import { useGingerTranscriptSync } from "@lucaismyname/ginger/transcript";
|
|
260
|
+
|
|
261
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
159
262
|
|
|
160
263
|
const vtt = `WEBVTT
|
|
161
264
|
|
|
@@ -164,11 +267,10 @@ Hello from VTT
|
|
|
164
267
|
`;
|
|
165
268
|
|
|
166
269
|
function TranscriptPanel() {
|
|
167
|
-
const {
|
|
270
|
+
const { activeCue, activeCues } = useGingerTranscriptSync({
|
|
168
271
|
transcript: vtt,
|
|
169
272
|
format: "auto",
|
|
170
273
|
});
|
|
171
|
-
|
|
172
274
|
return (
|
|
173
275
|
<div>
|
|
174
276
|
<p>Now: {activeCue?.text ?? "—"}</p>
|
|
@@ -177,26 +279,29 @@ function TranscriptPanel() {
|
|
|
177
279
|
);
|
|
178
280
|
}
|
|
179
281
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
282
|
+
export function App() {
|
|
283
|
+
return (
|
|
284
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
285
|
+
<Ginger.Player />
|
|
286
|
+
<Ginger.Control.PlayPause />
|
|
287
|
+
<TranscriptPanel />
|
|
288
|
+
</Ginger.Provider>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
183
291
|
```
|
|
184
292
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
### Multi-tab sync (`@lucaismyname/ginger/remote`)
|
|
188
|
-
|
|
189
|
-
Elects a **leader** tab via **`BroadcastChannel`** and pushes **`INIT`** snapshots to followers so queue and transport settings stay aligned. Mount **`Ginger.Player`** only on the leader so a single `<audio>` element plays.
|
|
293
|
+
#### <a id="subpath-starter-remote"></a> `@lucaismyname/ginger/remote`
|
|
190
294
|
|
|
191
295
|
```tsx
|
|
192
296
|
import { Ginger } from "@lucaismyname/ginger";
|
|
193
297
|
import { useGingerRemote } from "@lucaismyname/ginger/remote";
|
|
194
298
|
|
|
195
|
-
|
|
299
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
300
|
+
|
|
301
|
+
function RemoteShell() {
|
|
196
302
|
const { isLeader, isPending, error } = useGingerRemote({
|
|
197
303
|
channelName: "my-app-ginger",
|
|
198
304
|
});
|
|
199
|
-
|
|
200
305
|
return (
|
|
201
306
|
<>
|
|
202
307
|
{error && <p role="alert">{error}</p>}
|
|
@@ -205,86 +310,188 @@ function RemoteAwarePlayer() {
|
|
|
205
310
|
</>
|
|
206
311
|
);
|
|
207
312
|
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Snapshots send the current queue order with **`isShuffled: false`** so followers do not re-randomize; the visible order matches the leader. **`claimLeadership()`** requests leadership (lexicographically smaller tab IDs win conflicts).
|
|
211
313
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
314
|
+
export function App() {
|
|
315
|
+
return (
|
|
316
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
317
|
+
<RemoteShell />
|
|
318
|
+
{/* Transport controls still work in every tab */}
|
|
319
|
+
<Ginger.Control.PlayPause />
|
|
320
|
+
</Ginger.Provider>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
217
324
|
|
|
218
|
-
|
|
325
|
+
#### <a id="subpath-starter-cast"></a> `@lucaismyname/ginger/cast`
|
|
219
326
|
|
|
220
|
-
|
|
327
|
+
Cast needs **HTTPS** in production; use a real HTTPS `fileUrl` the receiver can fetch.
|
|
221
328
|
|
|
222
329
|
```tsx
|
|
223
330
|
import { Ginger } from "@lucaismyname/ginger";
|
|
224
331
|
import { useGingerCast } from "@lucaismyname/ginger/cast";
|
|
225
332
|
|
|
226
|
-
|
|
333
|
+
const tracks = [{ title: "Demo", fileUrl: "https://your.cdn/your-audio.mp3" }];
|
|
334
|
+
|
|
335
|
+
function CastShell() {
|
|
227
336
|
const { isCasting, requestSession, endSession, error } = useGingerCast();
|
|
228
337
|
return (
|
|
229
338
|
<>
|
|
230
339
|
{error && <p role="alert">{error}</p>}
|
|
231
|
-
<button type="button" onClick={() => void requestSession()}>
|
|
232
|
-
|
|
340
|
+
<button type="button" onClick={() => void requestSession()}>
|
|
341
|
+
Cast
|
|
342
|
+
</button>
|
|
343
|
+
<button type="button" onClick={endSession}>
|
|
344
|
+
Stop casting
|
|
345
|
+
</button>
|
|
233
346
|
{!isCasting && <Ginger.Player />}
|
|
234
347
|
</>
|
|
235
348
|
);
|
|
236
349
|
}
|
|
237
|
-
```
|
|
238
350
|
|
|
239
|
-
|
|
351
|
+
export function App() {
|
|
352
|
+
return (
|
|
353
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
354
|
+
<CastShell />
|
|
355
|
+
<Ginger.Control.PlayPause />
|
|
356
|
+
</Ginger.Provider>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
240
360
|
|
|
241
|
-
|
|
361
|
+
#### <a id="subpath-starter-crossfade"></a> `@lucaismyname/ginger/crossfade`
|
|
242
362
|
|
|
243
363
|
```tsx
|
|
364
|
+
import { Ginger } from "@lucaismyname/ginger";
|
|
244
365
|
import { useGingerCrossfade } from "@lucaismyname/ginger/crossfade";
|
|
245
366
|
|
|
246
|
-
|
|
367
|
+
const tracks = [
|
|
368
|
+
{ title: "A", fileUrl: "/your-audio-a.mp3" },
|
|
369
|
+
{ title: "B", fileUrl: "/your-audio-b.mp3" },
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
function CrossfadeReadout() {
|
|
247
373
|
const { status, error } = useGingerCrossfade({
|
|
248
374
|
enabled: true,
|
|
249
375
|
durationMs: 1200,
|
|
250
376
|
});
|
|
251
|
-
|
|
252
377
|
return (
|
|
253
378
|
<div>
|
|
254
|
-
<p>
|
|
379
|
+
<p>Crossfade: {status}</p>
|
|
255
380
|
{error && <p role="alert">{error}</p>}
|
|
256
381
|
</div>
|
|
257
382
|
);
|
|
258
383
|
}
|
|
384
|
+
|
|
385
|
+
export function App() {
|
|
386
|
+
return (
|
|
387
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
388
|
+
<Ginger.Player />
|
|
389
|
+
<Ginger.Control.PlayPause />
|
|
390
|
+
<Ginger.Control.Next />
|
|
391
|
+
<CrossfadeReadout />
|
|
392
|
+
</Ginger.Provider>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
259
395
|
```
|
|
260
396
|
|
|
261
|
-
|
|
397
|
+
#### <a id="subpath-starter-experimental-gapless"></a> `@lucaismyname/ginger/experimental-gapless`
|
|
262
398
|
|
|
263
|
-
|
|
399
|
+
Probe only; does **not** change playback.
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import { Ginger } from "@lucaismyname/ginger";
|
|
403
|
+
import { useExperimentalGapless } from "@lucaismyname/ginger/experimental-gapless";
|
|
404
|
+
|
|
405
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
264
406
|
|
|
265
|
-
|
|
407
|
+
function GaplessProbe() {
|
|
408
|
+
const { supported, reason, gingerGaplessPlayback, preloadedTrackIds } =
|
|
409
|
+
useExperimentalGapless();
|
|
410
|
+
return (
|
|
411
|
+
<pre style={{ fontSize: 12 }}>
|
|
412
|
+
{JSON.stringify(
|
|
413
|
+
{ supported, reason, gingerGaplessPlayback, preloadedTrackIds },
|
|
414
|
+
null,
|
|
415
|
+
2,
|
|
416
|
+
)}
|
|
417
|
+
</pre>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function App() {
|
|
422
|
+
return (
|
|
423
|
+
<Ginger.Provider initialTracks={tracks}>
|
|
424
|
+
<Ginger.Player />
|
|
425
|
+
<Ginger.Control.PlayPause />
|
|
426
|
+
<GaplessProbe />
|
|
427
|
+
</Ginger.Provider>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### <a id="subpath-starter-devtools"></a> `@lucaismyname/ginger/devtools`
|
|
433
|
+
|
|
434
|
+
`GingerDevtools` may sit **outside** `Ginger.Provider`; it discovers all registered players.
|
|
266
435
|
|
|
267
436
|
```tsx
|
|
437
|
+
import { Ginger } from "@lucaismyname/ginger";
|
|
268
438
|
import { GingerDevtools } from "@lucaismyname/ginger/devtools";
|
|
269
439
|
|
|
270
|
-
|
|
440
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
441
|
+
|
|
442
|
+
export function App() {
|
|
271
443
|
return (
|
|
272
444
|
<>
|
|
273
|
-
<Ginger.Provider debugLabel="Main
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<Ginger.Provider debugLabel="Ambient" initialTracks={ambientTracks}>
|
|
278
|
-
{/* ... */}
|
|
445
|
+
<Ginger.Provider debugLabel="Main" initialTracks={tracks}>
|
|
446
|
+
<Ginger.Player />
|
|
447
|
+
<Ginger.Control.PlayPause />
|
|
279
448
|
</Ginger.Provider>
|
|
280
|
-
|
|
281
|
-
{/* Single devtools instance — discovers both providers */}
|
|
282
449
|
<GingerDevtools />
|
|
283
450
|
</>
|
|
284
451
|
);
|
|
285
452
|
}
|
|
286
453
|
```
|
|
287
454
|
|
|
455
|
+
### Equalizer
|
|
456
|
+
|
|
457
|
+
**Full shell:** [Equalizer starter](#subpath-starter-equalizer) (above). The EQ and `useGingerLiveAnalyzer` share the same `AudioContext` and can be used together. EQ filters are inserted before the analyser in the Web Audio graph.
|
|
458
|
+
|
|
459
|
+
### Spatial audio (`@lucaismyname/ginger/spatial`)
|
|
460
|
+
|
|
461
|
+
Inserts an HRTF **`PannerNode`** into the same Web Audio graph as the EQ and live analyser (one `MediaElementAudioSourceNode` per `<audio>`). **Full shell:** [Spatial starter](#subpath-starter-spatial). Use **`setListenerPosition`** and **`setPanningModel`** for runtime updates without rebuilding the graph.
|
|
462
|
+
|
|
463
|
+
### Transcript (`@lucaismyname/ginger/transcript`)
|
|
464
|
+
|
|
465
|
+
Parse **SRT** and **WebVTT** captions and sync cues to playback time (podcasts, video-style transcripts). HTML tags in cue text are stripped. **Full shell:** [Transcript starter](#subpath-starter-transcript).
|
|
466
|
+
|
|
467
|
+
**`useGingerTranscriptSync`** mirrors **`useGingerLyricsSync`** but uses cue **start/end** ranges and exposes **`activeCues`** for overlapping captions. **`parseTranscriptAuto`** chooses VTT when the string starts with `WEBVTT`, otherwise SRT. Parse ahead of time with **`parseSrt`** / **`parseVtt`** when you do not need the hook.
|
|
468
|
+
|
|
469
|
+
### Multi-tab sync (`@lucaismyname/ginger/remote`)
|
|
470
|
+
|
|
471
|
+
Elects a **leader** tab via **`BroadcastChannel`** and pushes **`INIT`** snapshots to followers so queue and transport settings stay aligned. Mount **`Ginger.Player`** only on the leader so a single `<audio>` element plays. **Full shell:** [Remote starter](#subpath-starter-remote).
|
|
472
|
+
|
|
473
|
+
Snapshots send the current queue order with **`isShuffled: false`** so followers do not re-randomize; the visible order matches the leader. **`claimLeadership()`** requests leadership (lexicographically smaller tab IDs win conflicts).
|
|
474
|
+
|
|
475
|
+
`@lucaismyname/ginger/remote` also exports **`DEFAULT_REMOTE_CHANNEL_NAME`** and the **`RemoteMessage`** type if you need to share protocol constants or type your own channel helpers.
|
|
476
|
+
|
|
477
|
+
### Chromecast (`@lucaismyname/ginger/cast`)
|
|
478
|
+
|
|
479
|
+
Loads the **Google Cast Web Sender** (CAF), exposes **`useGingerCast`** for session + **`loadMedia`** sync, and helpers **`loadCastFramework`**, **`trackToMediaInfo`**, **`guessContentTypeFromUrl`**. The default receiver is the **Default Media Receiver**; override with **`receiverApplicationId`**.
|
|
480
|
+
|
|
481
|
+
**Platform:** Cast requires **HTTPS** in production (localhost is allowed for development). **`Track.fileUrl`** must be fetchable by the **Cast device** with correct **CORS**; avoid **mixed content**.
|
|
482
|
+
|
|
483
|
+
**Avoid double playback:** render **`{!isCasting && <Ginger.Player />}`** so the browser does not decode the same URLs as the TV. Optional **`syncLocalAudio: "pause-mute"`** mutes the local `<audio>` while connected. **Full shell:** [Cast starter](#subpath-starter-cast).
|
|
484
|
+
|
|
485
|
+
### Crossfade (`@lucaismyname/ginger/crossfade`)
|
|
486
|
+
|
|
487
|
+
Adds a Web Audio crossfade graph for **overlap-based** transitions between outgoing and incoming media. This is distinct from the longer-term **gapless** work: crossfade overlaps two sources on purpose, while gapless aims for seamless adjacent track boundaries on a single playback path. **Full shell:** [Crossfade starter](#subpath-starter-crossfade).
|
|
488
|
+
|
|
489
|
+
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.
|
|
490
|
+
|
|
491
|
+
### Devtools (`@lucaismyname/ginger/devtools`)
|
|
492
|
+
|
|
493
|
+
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>`. **Full shell:** [Devtools starter](#subpath-starter-devtools).
|
|
494
|
+
|
|
288
495
|
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.
|
|
289
496
|
|
|
290
497
|
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.
|
|
@@ -292,7 +499,7 @@ The panel uses Tailwind CSS via CDN (injected on mount, removed on unmount) and
|
|
|
292
499
|
### Experimental Notice
|
|
293
500
|
|
|
294
501
|
`@lucaismyname/ginger/experimental-gapless` is intentionally non-production.
|
|
295
|
-
It currently provides capability metadata only and does not alter playback behavior.
|
|
502
|
+
It currently provides capability metadata only and does not alter playback behavior. **Full shell:** [Experimental gapless starter](#subpath-starter-experimental-gapless).
|
|
296
503
|
|
|
297
504
|
## Release Process
|
|
298
505
|
|
|
@@ -1344,7 +1551,7 @@ Additional entrypoints:
|
|
|
1344
1551
|
- `@lucaismyname/ginger/experimental-gapless`
|
|
1345
1552
|
- `@lucaismyname/ginger/devtools`
|
|
1346
1553
|
|
|
1347
|
-
See [Subpath Exports](#subpath-exports) for
|
|
1554
|
+
See [Subpath Exports](#subpath-exports) for the import list, per-feature notes, and **[copy-paste starters](#subpath-copy-paste-starters)** for each subpath. `experimental-gapless` is explicitly non-production and does not alter core playback.
|
|
1348
1555
|
|
|
1349
1556
|
## Notes
|
|
1350
1557
|
|