@lucaismyname/ginger 0.0.59 → 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 CHANGED
@@ -64,11 +64,13 @@ For docs beyond this README, use the repository links below:
64
64
  - Streaming adapters: [`docs/guides/streaming-adapters.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/guides/streaming-adapters.md)
65
65
  - Components reference: [`docs/reference/components.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/reference/components.md)
66
66
  - Hooks reference: [`docs/reference/hooks.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/reference/hooks.md)
67
- - Subpath exports (waveform, EQ, spatial, transcript, remote, crossfade, …): [`docs/reference/subpaths.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/reference/subpaths.md)
67
+ - Subpath exports (waveform, EQ, spatial, transcript, remote, cast, crossfade, …): [`docs/reference/subpaths.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/reference/subpaths.md)
68
68
  - Generated API docs: [`docs/api/index.html`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/api/index.html)
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`
@@ -76,16 +78,106 @@ For docs beyond this README, use the repository links below:
76
78
  - `@lucaismyname/ginger/spatial`
77
79
  - `@lucaismyname/ginger/transcript`
78
80
  - `@lucaismyname/ginger/remote`
81
+ - `@lucaismyname/ginger/cast`
79
82
  - `@lucaismyname/ginger/crossfade`
80
83
  - `@lucaismyname/ginger/experimental-gapless`
81
84
  - `@lucaismyname/ginger/devtools`
82
85
 
83
- ### Equalizer
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`**.
84
114
 
85
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";
86
176
  import { useGingerEqualizer } from "@lucaismyname/ginger/equalizer";
87
177
 
88
- function MyPlayer() {
178
+ const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
179
+
180
+ function EqSliders() {
89
181
  const { setBandGain, bands, error } = useGingerEqualizer({
90
182
  bands: [
91
183
  { frequency: 60 },
@@ -95,7 +187,6 @@ function MyPlayer() {
95
187
  { frequency: 16000 },
96
188
  ],
97
189
  });
98
-
99
190
  return (
100
191
  <div>
101
192
  {bands.map((band, i) => (
@@ -114,24 +205,32 @@ function MyPlayer() {
114
205
  </div>
115
206
  );
116
207
  }
117
- ```
118
-
119
- 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.
120
208
 
121
- ### Spatial audio (`@lucaismyname/ginger/spatial`)
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
+ ```
122
219
 
123
- Inserts an HRTF **`PannerNode`** into the same Web Audio graph as the EQ and live analyser (one `MediaElementAudioSourceNode` per `<audio>`).
220
+ #### <a id="subpath-starter-spatial"></a> `@lucaismyname/ginger/spatial`
124
221
 
125
222
  ```tsx
223
+ import { Ginger } from "@lucaismyname/ginger";
126
224
  import { useGingerSpatialAudio } from "@lucaismyname/ginger/spatial";
127
225
 
128
- function Spatialized() {
226
+ const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
227
+
228
+ function SpatialControls() {
129
229
  const { setSourcePosition, error } = useGingerSpatialAudio({
130
230
  panningModel: "HRTF",
131
231
  position: [2, 0, 0],
132
232
  listenerPosition: [0, 0, 0],
133
233
  });
134
-
135
234
  return (
136
235
  <div>
137
236
  <button type="button" onClick={() => setSourcePosition(0, 0, -2)}>
@@ -141,20 +240,25 @@ function Spatialized() {
141
240
  </div>
142
241
  );
143
242
  }
144
- ```
145
-
146
- Use **`setListenerPosition`** and **`setPanningModel`** for runtime updates without rebuilding the graph.
147
243
 
148
- ### Transcript (`@lucaismyname/ginger/transcript`)
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
+ ```
149
254
 
150
- Parse **SRT** and **WebVTT** captions and sync cues to playback time (podcasts, video-style transcripts). HTML tags in cue text are stripped.
255
+ #### <a id="subpath-starter-transcript"></a> `@lucaismyname/ginger/transcript`
151
256
 
152
257
  ```tsx
153
- import {
154
- parseSrt,
155
- parseVtt,
156
- useGingerTranscriptSync,
157
- } 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" }];
158
262
 
159
263
  const vtt = `WEBVTT
160
264
 
@@ -163,11 +267,10 @@ Hello from VTT
163
267
  `;
164
268
 
165
269
  function TranscriptPanel() {
166
- const { cues, activeCue, activeCues } = useGingerTranscriptSync({
270
+ const { activeCue, activeCues } = useGingerTranscriptSync({
167
271
  transcript: vtt,
168
272
  format: "auto",
169
273
  });
170
-
171
274
  return (
172
275
  <div>
173
276
  <p>Now: {activeCue?.text ?? "—"}</p>
@@ -176,26 +279,29 @@ function TranscriptPanel() {
176
279
  );
177
280
  }
178
281
 
179
- // Or parse ahead of time:
180
- const cuesFromSrt = parseSrt(srtString);
181
- const cuesFromVtt = parseVtt(vttString);
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
+ }
182
291
  ```
183
292
 
184
- **`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.
185
-
186
- ### Multi-tab sync (`@lucaismyname/ginger/remote`)
187
-
188
- 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`
189
294
 
190
295
  ```tsx
191
296
  import { Ginger } from "@lucaismyname/ginger";
192
297
  import { useGingerRemote } from "@lucaismyname/ginger/remote";
193
298
 
194
- function RemoteAwarePlayer() {
299
+ const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
300
+
301
+ function RemoteShell() {
195
302
  const { isLeader, isPending, error } = useGingerRemote({
196
303
  channelName: "my-app-ginger",
197
304
  });
198
-
199
305
  return (
200
306
  <>
201
307
  {error && <p role="alert">{error}</p>}
@@ -204,61 +310,188 @@ function RemoteAwarePlayer() {
204
310
  </>
205
311
  );
206
312
  }
313
+
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
+ }
207
323
  ```
208
324
 
209
- 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).
325
+ #### <a id="subpath-starter-cast"></a> `@lucaismyname/ginger/cast`
210
326
 
211
- `@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.
327
+ Cast needs **HTTPS** in production; use a real HTTPS `fileUrl` the receiver can fetch.
212
328
 
213
- ### Crossfade (`@lucaismyname/ginger/crossfade`)
329
+ ```tsx
330
+ import { Ginger } from "@lucaismyname/ginger";
331
+ import { useGingerCast } from "@lucaismyname/ginger/cast";
332
+
333
+ const tracks = [{ title: "Demo", fileUrl: "https://your.cdn/your-audio.mp3" }];
334
+
335
+ function CastShell() {
336
+ const { isCasting, requestSession, endSession, error } = useGingerCast();
337
+ return (
338
+ <>
339
+ {error && <p role="alert">{error}</p>}
340
+ <button type="button" onClick={() => void requestSession()}>
341
+ Cast
342
+ </button>
343
+ <button type="button" onClick={endSession}>
344
+ Stop casting
345
+ </button>
346
+ {!isCasting && <Ginger.Player />}
347
+ </>
348
+ );
349
+ }
350
+
351
+ export function App() {
352
+ return (
353
+ <Ginger.Provider initialTracks={tracks}>
354
+ <CastShell />
355
+ <Ginger.Control.PlayPause />
356
+ </Ginger.Provider>
357
+ );
358
+ }
359
+ ```
214
360
 
215
- 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.
361
+ #### <a id="subpath-starter-crossfade"></a> `@lucaismyname/ginger/crossfade`
216
362
 
217
363
  ```tsx
364
+ import { Ginger } from "@lucaismyname/ginger";
218
365
  import { useGingerCrossfade } from "@lucaismyname/ginger/crossfade";
219
366
 
220
- function CrossfadeSetup() {
367
+ const tracks = [
368
+ { title: "A", fileUrl: "/your-audio-a.mp3" },
369
+ { title: "B", fileUrl: "/your-audio-b.mp3" },
370
+ ];
371
+
372
+ function CrossfadeReadout() {
221
373
  const { status, error } = useGingerCrossfade({
222
374
  enabled: true,
223
375
  durationMs: 1200,
224
376
  });
225
-
226
377
  return (
227
378
  <div>
228
- <p>Status: {status}</p>
379
+ <p>Crossfade: {status}</p>
229
380
  {error && <p role="alert">{error}</p>}
230
381
  </div>
231
382
  );
232
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
+ }
233
395
  ```
234
396
 
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.
397
+ #### <a id="subpath-starter-experimental-gapless"></a> `@lucaismyname/ginger/experimental-gapless`
236
398
 
237
- ### Devtools (`@lucaismyname/ginger/devtools`)
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" }];
238
406
 
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>`.
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.
240
435
 
241
436
  ```tsx
437
+ import { Ginger } from "@lucaismyname/ginger";
242
438
  import { GingerDevtools } from "@lucaismyname/ginger/devtools";
243
439
 
244
- function App() {
440
+ const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
441
+
442
+ export function App() {
245
443
  return (
246
444
  <>
247
- <Ginger.Provider debugLabel="Main Player" initialTracks={tracks}>
248
- {/* ... */}
249
- </Ginger.Provider>
250
-
251
- <Ginger.Provider debugLabel="Ambient" initialTracks={ambientTracks}>
252
- {/* ... */}
445
+ <Ginger.Provider debugLabel="Main" initialTracks={tracks}>
446
+ <Ginger.Player />
447
+ <Ginger.Control.PlayPause />
253
448
  </Ginger.Provider>
254
-
255
- {/* Single devtools instance — discovers both providers */}
256
449
  <GingerDevtools />
257
450
  </>
258
451
  );
259
452
  }
260
453
  ```
261
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
+
262
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.
263
496
 
264
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.
@@ -266,7 +499,7 @@ The panel uses Tailwind CSS via CDN (injected on mount, removed on unmount) and
266
499
  ### Experimental Notice
267
500
 
268
501
  `@lucaismyname/ginger/experimental-gapless` is intentionally non-production.
269
- 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).
270
503
 
271
504
  ## Release Process
272
505
 
@@ -1313,11 +1546,12 @@ Additional entrypoints:
1313
1546
  - `@lucaismyname/ginger/spatial`
1314
1547
  - `@lucaismyname/ginger/transcript`
1315
1548
  - `@lucaismyname/ginger/remote`
1549
+ - `@lucaismyname/ginger/cast`
1316
1550
  - `@lucaismyname/ginger/crossfade`
1317
1551
  - `@lucaismyname/ginger/experimental-gapless`
1318
1552
  - `@lucaismyname/ginger/devtools`
1319
1553
 
1320
- See [Subpath Exports](#subpath-exports) for **`spatial`**, **`transcript`**, **`remote`**, and **`devtools`** usage. `experimental-gapless` is explicitly non-production and does not alter core playback.
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.
1321
1555
 
1322
1556
  ## Notes
1323
1557
 
@@ -1339,7 +1573,7 @@ These priorities guide new work in the library; they are not a guarantee of ship
1339
1573
 
1340
1574
  1. **Music libraries and continuous listening** — Features that make track-to-track playback feel better come first: **next-track prefetch** (`useNextTrackPrefetch`), shipped **crossfade** (`@lucaismyname/ginger/crossfade`), ongoing **gapless** capability work (`@lucaismyname/ginger/experimental-gapless`), and first-class **chapter** / **synced lyrics** UI (`Ginger.Current.Chapters`, `Ginger.Current.LyricsSynced`).
1341
1575
  2. **Podcasts and live-style streams** — **HLS / DASH** integration is emphasized when a concrete app needs it; the core package stays on native `<audio>` with optional adapters or documentation rather than hard dependencies. **SRT / WebVTT** transcripts are supported via `@lucaismyname/ginger/transcript`.
1342
- 3. **Embedded or internal players** — **Accessibility**, persistence, and **testing** helpers are favored over heavier ecosystem integrations (Cast, proprietary cast SDKs) unless there is a dedicated use case. **Multi-tab** web apps can use `@lucaismyname/ginger/remote` (BroadcastChannel) before reaching for OS-level remote playback.
1576
+ 3. **Embedded or internal players** — **Accessibility**, persistence, and **testing** helpers are favored over heavier ecosystem integrations unless there is a dedicated use case. For **Chromecast**, opt‑in to `@lucaismyname/ginger/cast`. **Multi-tab** web apps can use `@lucaismyname/ginger/remote` (BroadcastChannel) before reaching for OS-level remote playback.
1343
1577
 
1344
1578
  ## Monorepo Development
1345
1579
 
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Minimal typings for the subset of the Google Cast Web Sender API used by Ginger.
3
+ * Runtime objects come from the loaded Cast Framework script (`loadCastFramework`).
4
+ */
5
+ /** Default Media Receiver application ID (audio/video). */
6
+ export declare const DEFAULT_MEDIA_RECEIVER_APP_ID = "CC1AD845";
7
+ export type CastImage = {
8
+ url: string;
9
+ width?: number;
10
+ height?: number;
11
+ };
12
+ /** Instance shape from `new chrome.cast.media.MusicTrackMediaMetadata()`. */
13
+ export type CastMusicTrackMetadata = {
14
+ metadataType: number;
15
+ title?: string;
16
+ artist?: string;
17
+ albumName?: string;
18
+ images?: CastImage[];
19
+ };
20
+ export type CastMediaInfoLike = {
21
+ contentId: string;
22
+ contentType: string;
23
+ streamType?: string;
24
+ metadata?: CastMusicTrackMetadata;
25
+ };
26
+ export type CastLoadRequestLike = {
27
+ media: CastMediaInfoLike;
28
+ autoplay?: boolean;
29
+ currentTime?: number;
30
+ };
31
+ export type CastMediaInfoConstructor = new (contentId: string, contentType: string) => CastMediaInfoLike;
32
+ export type CastMusicTrackMetadataConstructor = new () => CastMusicTrackMetadata;
33
+ export type CastLoadRequestConstructor = new (mediaInfo: CastMediaInfoLike) => CastLoadRequestLike;
34
+ /** Subset of `chrome.cast.Session` methods we call (duck-typed). */
35
+ export type CastSessionLike = {
36
+ loadMedia: (request: CastLoadRequestLike, successCallback: (media?: unknown) => void, errorCallback: (error: CastErrorLike) => void) => void;
37
+ endSession: (successCallback: () => void, errorCallback: (error: CastErrorLike) => void) => void;
38
+ };
39
+ export type CastErrorLike = {
40
+ code?: string;
41
+ description?: string;
42
+ };
43
+ export type CastContextOptionsLike = {
44
+ receiverApplicationId: string;
45
+ autoJoinPolicy?: number;
46
+ resumeSavedSession?: boolean;
47
+ };
48
+ /** `cast.framework.CastContext` duck type. */
49
+ export type CastFrameworkContextLike = {
50
+ setOptions: (options: CastContextOptionsLike) => void;
51
+ getCastState: () => number;
52
+ getCurrentSession: () => CastSessionLike | null;
53
+ addEventListener: (type: string, listener: (event: {
54
+ sessionState?: string;
55
+ }) => void) => void;
56
+ removeEventListener: (type: string, listener: (event: {
57
+ sessionState?: string;
58
+ }) => void) => void;
59
+ requestSession: () => Promise<void>;
60
+ };
61
+ export type CastFrameworkNamespace = {
62
+ CastContext: {
63
+ getInstance: () => CastFrameworkContextLike;
64
+ };
65
+ CastContextEventType: {
66
+ CAST_STATE_CHANGED: string;
67
+ SESSION_STATE_CHANGED: string;
68
+ };
69
+ CastState: {
70
+ NO_DEVICES: number;
71
+ NOT_CONNECTED: number;
72
+ CONNECTING: number;
73
+ CONNECTED: number;
74
+ };
75
+ SessionState: {
76
+ SESSION_STARTED: string;
77
+ SESSION_ENDED: string;
78
+ SESSION_RESUMED: string;
79
+ };
80
+ RemotePlayer: new () => CastRemotePlayerLike;
81
+ RemotePlayerController: new (player: CastRemotePlayerLike) => CastRemotePlayerControllerLike;
82
+ RemotePlayerEventType: {
83
+ IS_PAUSED_CHANGED: string;
84
+ CURRENT_TIME_CHANGED: string;
85
+ DURATION_CHANGED: string;
86
+ };
87
+ };
88
+ export type CastRemotePlayerLike = {
89
+ isPaused: boolean;
90
+ currentTime: number;
91
+ duration: number;
92
+ controller?: CastRemotePlayerControllerLike;
93
+ addEventListener: (type: string, handler: () => void) => void;
94
+ removeEventListener: (type: string, handler: () => void) => void;
95
+ };
96
+ export type CastRemotePlayerControllerLike = {
97
+ playOrPause: () => void;
98
+ seek: (options: {
99
+ value: number;
100
+ }) => void;
101
+ stop: () => void;
102
+ };
103
+ export type ChromeCastNamespace = {
104
+ media: {
105
+ DEFAULT_MEDIA_RECEIVER_APP_ID: string;
106
+ MediaInfo: CastMediaInfoConstructor;
107
+ MusicTrackMediaMetadata: CastMusicTrackMetadataConstructor;
108
+ LoadRequest: CastLoadRequestConstructor;
109
+ MetadataType: {
110
+ MUSIC_TRACK: number;
111
+ };
112
+ StreamType: {
113
+ BUFFERED: string;
114
+ };
115
+ };
116
+ isAvailable: boolean;
117
+ AutoJoinPolicy: {
118
+ ORIGIN_SCOPED: number;
119
+ TAB_AND_ORIGIN_SCOPED: number;
120
+ };
121
+ };
122
+ export type WindowWithCast = Window & {
123
+ cast?: {
124
+ framework?: CastFrameworkNamespace;
125
+ };
126
+ chrome?: {
127
+ cast?: ChromeCastNamespace;
128
+ };
129
+ };
130
+ //# sourceMappingURL=castTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"castTypes.d.ts","sourceRoot":"","sources":["../../src/cast/castTypes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("react"),oe=require("../useGinger-BMRLzjmr.cjs"),ue=require("../selectors-CEGlYoFu.cjs"),J="CC1AD845",q="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";let b=null;function ae(){if(!(typeof window>"u"))return window}function M(){var e,i;const u=ae();return u?(i=(e=u.cast)==null?void 0:e.framework)!=null&&i.CastContext?Promise.resolve():b||(b=new Promise((f,c)=>{const l=document.querySelector(`script[src="${q}"]`);if(l){const T=()=>{var p,a;(a=(p=u.cast)==null?void 0:p.framework)!=null&&a.CastContext?f():c(new Error("Cast script loaded but cast.framework is missing."))};if(l.dataset.gingerCastLoaded==="true"){T();return}l.addEventListener("load",T,{once:!0}),l.addEventListener("error",()=>c(new Error("Cast script failed to load.")),{once:!0});return}const m=document.createElement("script");m.src=q,m.async=!0,m.addEventListener("load",()=>{var T,p;m.dataset.gingerCastLoaded="true",(p=(T=u.cast)==null?void 0:T.framework)!=null&&p.CastContext?f():c(new Error("Cast script loaded but cast.framework is missing."))}),m.addEventListener("error",()=>{c(new Error("Cast script failed to load."))}),document.head.appendChild(m)}),b):Promise.reject(new Error("Cast is only available in a browser environment."))}const ie={".mp3":"audio/mpeg",".aac":"audio/aac",".m4a":"audio/mp4",".ogg":"audio/ogg",".opus":"audio/opus",".wav":"audio/wav",".flac":"audio/flac",".webm":"audio/webm"};function K(u){try{const e=new URL(u,"https://example.com").pathname.toLowerCase(),i=e.lastIndexOf(".");if(i===-1)return"audio/mpeg";const f=e.slice(i);return ie[f]??"audio/mpeg"}catch{return"audio/mpeg"}}function $(u,e,i){var m;const f=((m=i==null?void 0:i.contentTypeResolver)==null?void 0:m.call(i,e))??K(e.fileUrl),c=new u.media.MediaInfo(e.fileUrl,f);c.streamType=u.media.StreamType.BUFFERED;const l=new u.media.MusicTrackMediaMetadata;return l.metadataType=u.media.MetadataType.MUSIC_TRACK,l.title=e.title,e.artist&&(l.artist=e.artist),e.album&&(l.albumName=e.album),e.artworkUrl&&(l.images=[{url:e.artworkUrl}]),c.metadata=l,new u.media.LoadRequest(c)}const ce=2;function A(u){var f,c;const e=(f=u.cast)==null?void 0:f.framework,i=(c=u.chrome)==null?void 0:c.cast;return!(e!=null&&e.CastContext)||!(i!=null&&i.media)?null:{context:e.CastContext.getInstance(),framework:e,chromeCast:i}}function le(u){if(u instanceof Error)return u.message;if(u&&typeof u=="object"&&"description"in u){const e=u.description;if(typeof e=="string"&&e.length>0)return e}return"Unknown Cast error"}function de(u={}){const{enabled:e=!0,receiverApplicationId:i=J,autoJoinPolicy:f,resumeSavedSession:c=!0,syncLocalAudio:l="pause-mute",endSessionOnUnmount:m=!1,contentTypeResolver:T,onError:p}=u,{state:a,audioRef:R}=oe.useGinger(),y=ue.getCurrentTrack(a),[w,j]=n.useState(!1),[B,V]=n.useState(!1),[E,W]=n.useState(!1),[X,z]=n.useState(null),[Q,L]=n.useState(null),[Y,Z]=n.useState(0),[ee,te]=n.useState(0),[ne,re]=n.useState(!0),U=n.useRef(null),_=n.useRef(null),g=n.useRef(null),P=n.useRef(0),N=n.useRef(-1),S=n.useRef(null),G=n.useRef(0),O=n.useRef(a.currentTime);O.current=a.currentTime;const D=n.useRef(p);D.current=p;const v=n.useCallback(s=>{var r;const t=le(s);L(t);try{(r=D.current)==null||r.call(D,(s instanceof Error,s))}catch{}},[]);n.useEffect(()=>{if(!e)return;let s=!1;return M().then(()=>{var r,o;if(s)return;j(!0),V(!!((o=(r=window.chrome)==null?void 0:r.cast)!=null&&o.isAvailable))}).catch(t=>{s||v(t instanceof Error?t:new Error(String(t)))}),()=>{s=!0}},[e,v]),n.useEffect(()=>{var H;if(!e||!w)return;const t=A(window);if(!t)return;const{context:r,framework:o,chromeCast:I}=t,d=f??((H=I.AutoJoinPolicy)==null?void 0:H.ORIGIN_SCOPED)??1;r.setOptions({receiverApplicationId:i,autoJoinPolicy:d,resumeSavedSession:c});const C=()=>{const x=r.getCastState()===o.CastState.CONNECTED;W(x)},h=()=>C(),F=x=>{x.sessionState&&z(x.sessionState),C()};return r.addEventListener(o.CastContextEventType.CAST_STATE_CHANGED,h),r.addEventListener(o.CastContextEventType.SESSION_STATE_CHANGED,F),C(),()=>{r.removeEventListener(o.CastContextEventType.CAST_STATE_CHANGED,h),r.removeEventListener(o.CastContextEventType.SESSION_STATE_CHANGED,F)}},[e,w,i,f,c]),n.useEffect(()=>{if(!e||!w)return;const t=A(window);if(!t)return;const{framework:r}=t,o=new r.RemotePlayer,I=new r.RemotePlayerController(o);U.current=o,_.current=I;const d=()=>{Z(o.currentTime),te(o.duration),re(o.isPaused)},{RemotePlayerEventType:C}=r;return o.addEventListener(C.IS_PAUSED_CHANGED,d),o.addEventListener(C.CURRENT_TIME_CHANGED,d),o.addEventListener(C.DURATION_CHANGED,d),d(),()=>{o.removeEventListener(C.IS_PAUSED_CHANGED,d),o.removeEventListener(C.CURRENT_TIME_CHANGED,d),o.removeEventListener(C.DURATION_CHANGED,d),U.current=null,_.current=null}},[e,w]),n.useEffect(()=>{if(!e||l==="none")return;const s=R.current;if(!E||!s){S.current&&s&&(s.muted=S.current.muted,s.volume=S.current.volume,S.current=null);return}return S.current||(S.current={volume:s.volume,muted:s.muted}),s.muted=!0,s.volume=0,()=>{S.current&&R.current&&(R.current.muted=S.current.muted,R.current.volume=S.current.volume,S.current=null)}},[e,l,E,R]),n.useEffect(()=>{if(!e||!w||!E)return;const t=A(window);if(!t)return;const r=t.context.getCurrentSession();if(!r||!y)return;const o=`${a.currentIndex}:${y.fileUrl}`,I=t.chromeCast;if(g.current===o)return;g.current=o,G.current=Date.now()+500;const d=$(I,y,{contentTypeResolver:T});d.autoplay=!a.isPaused,d.currentTime=0,r.loadMedia(d,()=>{N.current=a.currentIndex,P.current=O.current},C=>v(C))},[e,w,E,a.currentIndex,a.isPaused,y,T,v]),n.useEffect(()=>{E||(g.current=null)},[E]),n.useEffect(()=>{if(!e||!w||!E||!y)return;const s=`${a.currentIndex}:${y.fileUrl}`;if(g.current!==s||Date.now()<G.current)return;const t=U.current,r=_.current;!t||!r||t.isPaused!==a.isPaused&&r.playOrPause()},[e,w,E,a.isPaused,a.currentIndex,y]),n.useEffect(()=>{var t;if(!e||!E)return;if(N.current!==a.currentIndex){N.current=a.currentIndex,P.current=a.currentTime;return}Math.abs(a.currentTime-P.current)>ce&&((t=_.current)==null||t.seek({value:a.currentTime})),P.current=a.currentTime},[e,E,a.currentTime,a.currentIndex]);const se=n.useCallback(async()=>{w||await M();const t=A(window);if(!t){v(new Error("Cast framework is not available."));return}try{await t.context.requestSession(),L(null)}catch(r){v(r)}},[v,w]),k=n.useCallback(()=>{const t=A(window),r=t==null?void 0:t.context.getCurrentSession();r&&r.endSession(()=>{g.current=null,L(null)},o=>v(o))},[v]);return n.useEffect(()=>{if(m)return()=>{k()}},[m,k]),{isAvailable:B,isConnected:E,isCasting:E,sessionState:X,error:Q,requestSession:se,endSession:k,receiverCurrentTime:Y,receiverDuration:ee,receiverIsPaused:ne}}exports.DEFAULT_MEDIA_RECEIVER_APP_ID=J;exports.guessContentTypeFromUrl=K;exports.loadCastFramework=M;exports.trackToMediaInfo=$;exports.useGingerCast=de;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../../src/cast/castTypes.ts","../../src/cast/loadCastFramework.ts","../../src/cast/trackToMediaInfo.ts","../../src/cast/useGingerCast.ts"],"sourcesContent":["/**\n * Minimal typings for the subset of the Google Cast Web Sender API used by Ginger.\n * Runtime objects come from the loaded Cast Framework script (`loadCastFramework`).\n */\n\n/** Default Media Receiver application ID (audio/video). */\nexport const DEFAULT_MEDIA_RECEIVER_APP_ID = \"CC1AD845\";\n\nexport type CastImage = {\n url: string;\n width?: number;\n height?: number;\n};\n\n/** Instance shape from `new chrome.cast.media.MusicTrackMediaMetadata()`. */\nexport type CastMusicTrackMetadata = {\n metadataType: number;\n title?: string;\n artist?: string;\n albumName?: string;\n images?: CastImage[];\n};\n\nexport type CastMediaInfoLike = {\n contentId: string;\n contentType: string;\n streamType?: string;\n metadata?: CastMusicTrackMetadata;\n};\n\nexport type CastLoadRequestLike = {\n media: CastMediaInfoLike;\n autoplay?: boolean;\n currentTime?: number;\n};\n\nexport type CastMediaInfoConstructor = new (\n contentId: string,\n contentType: string,\n) => CastMediaInfoLike;\nexport type CastMusicTrackMetadataConstructor = new () => CastMusicTrackMetadata;\nexport type CastLoadRequestConstructor = new (mediaInfo: CastMediaInfoLike) => CastLoadRequestLike;\n\n/** Subset of `chrome.cast.Session` methods we call (duck-typed). */\nexport type CastSessionLike = {\n loadMedia: (\n request: CastLoadRequestLike,\n successCallback: (media?: unknown) => void,\n errorCallback: (error: CastErrorLike) => void,\n ) => void;\n endSession: (successCallback: () => void, errorCallback: (error: CastErrorLike) => void) => void;\n};\n\nexport type CastErrorLike = {\n code?: string;\n description?: string;\n};\n\nexport type CastContextOptionsLike = {\n receiverApplicationId: string;\n autoJoinPolicy?: number;\n resumeSavedSession?: boolean;\n};\n\n/** `cast.framework.CastContext` duck type. */\nexport type CastFrameworkContextLike = {\n setOptions: (options: CastContextOptionsLike) => void;\n getCastState: () => number;\n getCurrentSession: () => CastSessionLike | null;\n addEventListener: (type: string, listener: (event: { sessionState?: string }) => void) => void;\n removeEventListener: (type: string, listener: (event: { sessionState?: string }) => void) => void;\n requestSession: () => Promise<void>;\n};\n\nexport type CastFrameworkNamespace = {\n CastContext: {\n getInstance: () => CastFrameworkContextLike;\n };\n CastContextEventType: {\n CAST_STATE_CHANGED: string;\n SESSION_STATE_CHANGED: string;\n };\n CastState: {\n NO_DEVICES: number;\n NOT_CONNECTED: number;\n CONNECTING: number;\n CONNECTED: number;\n };\n SessionState: {\n SESSION_STARTED: string;\n SESSION_ENDED: string;\n SESSION_RESUMED: string;\n };\n RemotePlayer: new () => CastRemotePlayerLike;\n RemotePlayerController: new (player: CastRemotePlayerLike) => CastRemotePlayerControllerLike;\n RemotePlayerEventType: {\n IS_PAUSED_CHANGED: string;\n CURRENT_TIME_CHANGED: string;\n DURATION_CHANGED: string;\n };\n};\n\nexport type CastRemotePlayerLike = {\n isPaused: boolean;\n currentTime: number;\n duration: number;\n controller?: CastRemotePlayerControllerLike;\n addEventListener: (type: string, handler: () => void) => void;\n removeEventListener: (type: string, handler: () => void) => void;\n};\n\nexport type CastRemotePlayerControllerLike = {\n playOrPause: () => void;\n seek: (options: { value: number }) => void;\n stop: () => void;\n};\n\nexport type ChromeCastNamespace = {\n media: {\n DEFAULT_MEDIA_RECEIVER_APP_ID: string;\n MediaInfo: CastMediaInfoConstructor;\n MusicTrackMediaMetadata: CastMusicTrackMetadataConstructor;\n LoadRequest: CastLoadRequestConstructor;\n MetadataType: {\n MUSIC_TRACK: number;\n };\n StreamType: {\n BUFFERED: string;\n };\n };\n isAvailable: boolean;\n AutoJoinPolicy: {\n ORIGIN_SCOPED: number;\n TAB_AND_ORIGIN_SCOPED: number;\n };\n};\n\nexport type WindowWithCast = Window & {\n cast?: { framework?: CastFrameworkNamespace };\n chrome?: { cast?: ChromeCastNamespace };\n};\n","import type { WindowWithCast } from \"./castTypes\";\n\nconst CAST_SCRIPT_SRC =\n \"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1\";\n\nlet loadPromise: Promise<void> | null = null;\n\nfunction getWindow(): WindowWithCast | undefined {\n if (typeof window === \"undefined\") return undefined;\n return window as WindowWithCast;\n}\n\n/**\n * Loads the Google Cast Web Sender script (Cast Application Framework) once.\n * Resolves when `window.cast.framework` is available. Safe to call multiple times.\n *\n * In SSR environments (no `window`), rejects with an error.\n */\nexport function loadCastFramework(): Promise<void> {\n const win = getWindow();\n if (!win) {\n return Promise.reject(new Error(\"Cast is only available in a browser environment.\"));\n }\n\n if (win.cast?.framework?.CastContext) {\n return Promise.resolve();\n }\n\n if (loadPromise) {\n return loadPromise;\n }\n\n loadPromise = new Promise((resolve, reject) => {\n const existing = document.querySelector(`script[src=\"${CAST_SCRIPT_SRC}\"]`);\n if (existing) {\n const onLoad = () => {\n if (win.cast?.framework?.CastContext) {\n resolve();\n } else {\n reject(new Error(\"Cast script loaded but cast.framework is missing.\"));\n }\n };\n if ((existing as HTMLScriptElement).dataset.gingerCastLoaded === \"true\") {\n onLoad();\n return;\n }\n existing.addEventListener(\"load\", onLoad, { once: true });\n existing.addEventListener(\"error\", () => reject(new Error(\"Cast script failed to load.\")), {\n once: true,\n });\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = CAST_SCRIPT_SRC;\n script.async = true;\n script.addEventListener(\"load\", () => {\n script.dataset.gingerCastLoaded = \"true\";\n if (win.cast?.framework?.CastContext) {\n resolve();\n } else {\n reject(new Error(\"Cast script loaded but cast.framework is missing.\"));\n }\n });\n script.addEventListener(\"error\", () => {\n reject(new Error(\"Cast script failed to load.\"));\n });\n document.head.appendChild(script);\n });\n\n return loadPromise;\n}\n","import type { Track } from \"../types\";\nimport type { CastLoadRequestLike, ChromeCastNamespace } from \"./castTypes\";\n\nconst EXT_TO_MIME: Record<string, string> = {\n \".mp3\": \"audio/mpeg\",\n \".aac\": \"audio/aac\",\n \".m4a\": \"audio/mp4\",\n \".ogg\": \"audio/ogg\",\n \".opus\": \"audio/opus\",\n \".wav\": \"audio/wav\",\n \".flac\": \"audio/flac\",\n \".webm\": \"audio/webm\",\n};\n\n/**\n * Guess a MIME type from a file URL path. Falls back to `audio/mpeg`.\n */\nexport function guessContentTypeFromUrl(fileUrl: string): string {\n try {\n const path = new URL(fileUrl, \"https://example.com\").pathname.toLowerCase();\n const dot = path.lastIndexOf(\".\");\n if (dot === -1) return \"audio/mpeg\";\n const ext = path.slice(dot);\n return EXT_TO_MIME[ext] ?? \"audio/mpeg\";\n } catch {\n return \"audio/mpeg\";\n }\n}\n\n/**\n * Builds a Cast `LoadRequest` for the given track using the runtime `chrome.cast.media` constructors.\n * Call only after `loadCastFramework()` and when `chrome.cast` is defined.\n */\nexport function trackToMediaInfo(\n chromeCast: ChromeCastNamespace,\n track: Track,\n options?: {\n contentTypeResolver?: (t: Track) => string;\n },\n): CastLoadRequestLike {\n const contentType =\n options?.contentTypeResolver?.(track) ?? guessContentTypeFromUrl(track.fileUrl);\n\n const media = new chromeCast.media.MediaInfo(track.fileUrl, contentType);\n media.streamType = chromeCast.media.StreamType.BUFFERED;\n\n const meta = new chromeCast.media.MusicTrackMediaMetadata();\n meta.metadataType = chromeCast.media.MetadataType.MUSIC_TRACK;\n meta.title = track.title;\n if (track.artist) {\n meta.artist = track.artist;\n }\n if (track.album) {\n meta.albumName = track.album;\n }\n if (track.artworkUrl) {\n meta.images = [{ url: track.artworkUrl }];\n }\n\n media.metadata = meta;\n\n return new chromeCast.media.LoadRequest(media);\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport { getCurrentTrack } from \"../internal/selectors\";\nimport type { Track } from \"../types\";\nimport type { CastErrorLike, WindowWithCast } from \"./castTypes\";\nimport { DEFAULT_MEDIA_RECEIVER_APP_ID } from \"./castTypes\";\nimport { loadCastFramework } from \"./loadCastFramework\";\nimport { trackToMediaInfo } from \"./trackToMediaInfo\";\n\nconst SEEK_JUMP_SECONDS = 2;\n\nfunction getCastGlobals(win: WindowWithCast): {\n context: import(\"./castTypes\").CastFrameworkContextLike;\n framework: import(\"./castTypes\").CastFrameworkNamespace;\n chromeCast: import(\"./castTypes\").ChromeCastNamespace;\n} | null {\n const framework = win.cast?.framework;\n const chromeCast = win.chrome?.cast;\n if (!framework?.CastContext || !chromeCast?.media) return null;\n return {\n context: framework.CastContext.getInstance(),\n framework,\n chromeCast,\n };\n}\n\nexport type UseGingerCastOptions = {\n /** When false, the hook does not touch Cast APIs. Default: true. */\n enabled?: boolean;\n /** Receiver app ID. Default: Default Media Receiver (`CC1AD845`). */\n receiverApplicationId?: string;\n /** Passed to `CastContext.setOptions`. Default: `chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED`. */\n autoJoinPolicy?: number;\n /** Passed to `CastContext.setOptions`. Default: true. */\n resumeSavedSession?: boolean;\n /**\n * When connected, silences the local `<audio>` (`muted` + `volume = 0`) so it does not compete\n * with the TV. Restored on disconnect. Does **not** change Ginger playback state.\n * Default: `\"pause-mute\"` (silence local element).\n */\n syncLocalAudio?: \"pause-mute\" | \"none\";\n /** When true, ends the Cast session when the hook unmounts. Default: false. */\n endSessionOnUnmount?: boolean;\n contentTypeResolver?: (track: Track) => string;\n onError?: (error: CastErrorLike | Error) => void;\n};\n\nexport type UseGingerCastResult = {\n /** Cast APIs loaded and `chrome.cast` is present (may still be `false` on unsupported browsers). */\n isAvailable: boolean;\n /** Cast sender is connected to a receiver. */\n isConnected: boolean;\n /** Same as `isConnected`; use to gate `Ginger.Player` (`{!isCasting && <Ginger.Player />}`). */\n isCasting: boolean;\n /** Last `SESSION_STATE_CHANGED` value from CAF, if any. */\n sessionState: string | null;\n error: string | null;\n /** Starts a Cast session (shows the Cast device picker when needed). */\n requestSession: () => Promise<void>;\n /** Ends the current Cast session, if any. */\n endSession: () => void;\n /** Position reported by `RemotePlayer` on the sender (useful if local audio is unmounted). */\n receiverCurrentTime: number;\n /** Duration reported by `RemotePlayer`. */\n receiverDuration: number;\n /** Pause state reported by `RemotePlayer`. */\n receiverIsPaused: boolean;\n};\n\nfunction formatCastError(err: CastErrorLike | Error | unknown): string {\n if (err instanceof Error) return err.message;\n if (err && typeof err === \"object\" && \"description\" in err) {\n const d = (err as CastErrorLike).description;\n if (typeof d === \"string\" && d.length > 0) return d;\n }\n return \"Unknown Cast error\";\n}\n\n/**\n * Chromecast Web Sender (CAF) bridge: loads the current Ginger track on a Cast receiver and keeps\n * transport roughly in sync while Ginger remains the queue source of truth.\n *\n * Prefer **`{!isCasting && <Ginger.Player />}`** so local `<audio>` does not decode the same URLs\n * as the receiver. When you must keep `Ginger.Player` mounted, use `syncLocalAudio: \"pause-mute\"`\n * to mute the local element.\n *\n * ```ts\n * import { useGingerCast } from \"@lucaismyname/ginger/cast\";\n * ```\n */\nexport function useGingerCast(options: UseGingerCastOptions = {}): UseGingerCastResult {\n const {\n enabled = true,\n receiverApplicationId = DEFAULT_MEDIA_RECEIVER_APP_ID,\n autoJoinPolicy: autoJoinPolicyOpt,\n resumeSavedSession = true,\n syncLocalAudio = \"pause-mute\",\n endSessionOnUnmount = false,\n contentTypeResolver,\n onError,\n } = options;\n\n const { state, audioRef } = useGinger();\n const currentTrack = getCurrentTrack(state);\n\n const [frameworkReady, setFrameworkReady] = useState(false);\n const [isAvailable, setIsAvailable] = useState(false);\n const [isConnected, setIsConnected] = useState(false);\n const [sessionState, setSessionState] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [receiverCurrentTime, setReceiverCurrentTime] = useState(0);\n const [receiverDuration, setReceiverDuration] = useState(0);\n const [receiverIsPaused, setReceiverIsPaused] = useState(true);\n\n const remotePlayerRef = useRef<import(\"./castTypes\").CastRemotePlayerLike | null>(null);\n const remoteControllerRef = useRef<import(\"./castTypes\").CastRemotePlayerControllerLike | null>(\n null,\n );\n const lastMediaKeyRef = useRef<string | null>(null);\n const prevSeekTimeRef = useRef(0);\n const prevSeekIndexRef = useRef<number>(-1);\n const localsRef = useRef<{ volume: number; muted: boolean } | null>(null);\n const skipPlayPauseUntilRef = useRef(0);\n const stateCurrentTimeRef = useRef(state.currentTime);\n stateCurrentTimeRef.current = state.currentTime;\n\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n\n const emitError = useCallback((err: CastErrorLike | Error | unknown) => {\n const msg = formatCastError(err);\n setError(msg);\n try {\n onErrorRef.current?.(err instanceof Error ? err : (err as CastErrorLike));\n } catch {\n /* ignore user handler errors */\n }\n }, []);\n\n // Load CAF once when enabled.\n useEffect(() => {\n if (!enabled) return;\n let cancelled = false;\n void loadCastFramework()\n .then(() => {\n if (cancelled) return;\n setFrameworkReady(true);\n const win = window as WindowWithCast;\n setIsAvailable(Boolean(win.chrome?.cast?.isAvailable));\n })\n .catch((e: unknown) => {\n if (cancelled) return;\n emitError(e instanceof Error ? e : new Error(String(e)));\n });\n return () => {\n cancelled = true;\n };\n }, [enabled, emitError]);\n\n // CastContext options + connection listeners.\n useEffect(() => {\n if (!enabled || !frameworkReady) return;\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) return;\n\n const { context, framework, chromeCast } = globals;\n const autoJoinPolicy = autoJoinPolicyOpt ?? chromeCast.AutoJoinPolicy?.ORIGIN_SCOPED ?? 1;\n\n context.setOptions({\n receiverApplicationId,\n autoJoinPolicy,\n resumeSavedSession,\n });\n\n const syncConnected = () => {\n const connected = context.getCastState() === framework.CastState.CONNECTED;\n setIsConnected(connected);\n };\n\n const onCastState = () => syncConnected();\n const onSessionState = (e: { sessionState?: string }) => {\n if (e.sessionState) setSessionState(e.sessionState);\n syncConnected();\n };\n\n context.addEventListener(framework.CastContextEventType.CAST_STATE_CHANGED, onCastState);\n context.addEventListener(framework.CastContextEventType.SESSION_STATE_CHANGED, onSessionState);\n syncConnected();\n\n return () => {\n context.removeEventListener(framework.CastContextEventType.CAST_STATE_CHANGED, onCastState);\n context.removeEventListener(\n framework.CastContextEventType.SESSION_STATE_CHANGED,\n onSessionState,\n );\n };\n }, [enabled, frameworkReady, receiverApplicationId, autoJoinPolicyOpt, resumeSavedSession]);\n\n // RemotePlayer (for seek / play/pause / progress mirrors).\n useEffect(() => {\n if (!enabled || !frameworkReady) return;\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) return;\n\n const { framework } = globals;\n const player = new framework.RemotePlayer();\n const controller = new framework.RemotePlayerController(player);\n remotePlayerRef.current = player;\n remoteControllerRef.current = controller;\n\n const bump = () => {\n setReceiverCurrentTime(player.currentTime);\n setReceiverDuration(player.duration);\n setReceiverIsPaused(player.isPaused);\n };\n\n const { RemotePlayerEventType } = framework;\n player.addEventListener(RemotePlayerEventType.IS_PAUSED_CHANGED, bump);\n player.addEventListener(RemotePlayerEventType.CURRENT_TIME_CHANGED, bump);\n player.addEventListener(RemotePlayerEventType.DURATION_CHANGED, bump);\n bump();\n\n return () => {\n player.removeEventListener(RemotePlayerEventType.IS_PAUSED_CHANGED, bump);\n player.removeEventListener(RemotePlayerEventType.CURRENT_TIME_CHANGED, bump);\n player.removeEventListener(RemotePlayerEventType.DURATION_CHANGED, bump);\n remotePlayerRef.current = null;\n remoteControllerRef.current = null;\n };\n }, [enabled, frameworkReady]);\n\n // Silence local <audio> while casting (optional).\n useEffect(() => {\n if (!enabled || syncLocalAudio === \"none\") return;\n const el = audioRef.current;\n if (!isConnected || !el) {\n if (localsRef.current && el) {\n el.muted = localsRef.current.muted;\n el.volume = localsRef.current.volume;\n localsRef.current = null;\n }\n return;\n }\n if (!localsRef.current) {\n localsRef.current = { volume: el.volume, muted: el.muted };\n }\n el.muted = true;\n el.volume = 0;\n return () => {\n if (localsRef.current && audioRef.current) {\n audioRef.current.muted = localsRef.current.muted;\n audioRef.current.volume = localsRef.current.volume;\n localsRef.current = null;\n }\n };\n }, [enabled, syncLocalAudio, isConnected, audioRef]);\n\n // Load media when the active track changes.\n useEffect(() => {\n if (!enabled || !frameworkReady || !isConnected) return;\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) return;\n const session = globals.context.getCurrentSession();\n if (!session || !currentTrack) return;\n\n const mediaKey = `${state.currentIndex}:${currentTrack.fileUrl}`;\n const chromeCast = globals.chromeCast;\n\n if (lastMediaKeyRef.current === mediaKey) return;\n\n lastMediaKeyRef.current = mediaKey;\n skipPlayPauseUntilRef.current = Date.now() + 500;\n const loadRequest = trackToMediaInfo(chromeCast, currentTrack, { contentTypeResolver });\n loadRequest.autoplay = !state.isPaused;\n loadRequest.currentTime = 0;\n\n session.loadMedia(\n loadRequest,\n () => {\n prevSeekIndexRef.current = state.currentIndex;\n prevSeekTimeRef.current = stateCurrentTimeRef.current;\n },\n (err) => emitError(err),\n );\n }, [\n enabled,\n frameworkReady,\n isConnected,\n state.currentIndex,\n state.isPaused,\n currentTrack,\n contentTypeResolver,\n emitError,\n ]);\n\n useEffect(() => {\n if (!isConnected) {\n lastMediaKeyRef.current = null;\n }\n }, [isConnected]);\n\n // Play/pause on the receiver when Ginger pause state changes (same track only).\n useEffect(() => {\n if (!enabled || !frameworkReady || !isConnected) return;\n if (!currentTrack) return;\n const mediaKey = `${state.currentIndex}:${currentTrack.fileUrl}`;\n if (lastMediaKeyRef.current !== mediaKey) return;\n if (Date.now() < skipPlayPauseUntilRef.current) return;\n\n const player = remotePlayerRef.current;\n const controller = remoteControllerRef.current;\n if (!player || !controller) return;\n if (player.isPaused !== state.isPaused) {\n controller.playOrPause();\n }\n }, [enabled, frameworkReady, isConnected, state.isPaused, state.currentIndex, currentTrack]);\n\n // Map large local time jumps to receiver seek (e.g. scrubbing).\n useEffect(() => {\n if (!enabled || !isConnected) return;\n if (prevSeekIndexRef.current !== state.currentIndex) {\n prevSeekIndexRef.current = state.currentIndex;\n prevSeekTimeRef.current = state.currentTime;\n return;\n }\n const jump = Math.abs(state.currentTime - prevSeekTimeRef.current);\n if (jump > SEEK_JUMP_SECONDS) {\n remoteControllerRef.current?.seek({ value: state.currentTime });\n }\n prevSeekTimeRef.current = state.currentTime;\n }, [enabled, isConnected, state.currentTime, state.currentIndex]);\n\n const requestSession = useCallback(async () => {\n if (!frameworkReady) {\n await loadCastFramework();\n }\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) {\n emitError(new Error(\"Cast framework is not available.\"));\n return;\n }\n try {\n await globals.context.requestSession();\n setError(null);\n } catch (e) {\n emitError(e);\n }\n }, [emitError, frameworkReady]);\n\n const endSession = useCallback(() => {\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n const session = globals?.context.getCurrentSession() as\n | import(\"./castTypes\").CastSessionLike\n | null;\n if (!session) return;\n session.endSession(\n () => {\n lastMediaKeyRef.current = null;\n setError(null);\n },\n (err) => emitError(err),\n );\n }, [emitError]);\n\n useEffect(() => {\n if (!endSessionOnUnmount) return;\n return () => {\n endSession();\n };\n }, [endSessionOnUnmount, endSession]);\n\n return {\n isAvailable,\n isConnected,\n isCasting: isConnected,\n sessionState,\n error,\n requestSession,\n endSession,\n receiverCurrentTime,\n receiverDuration,\n receiverIsPaused,\n };\n}\n"],"names":["DEFAULT_MEDIA_RECEIVER_APP_ID","CAST_SCRIPT_SRC","loadPromise","getWindow","loadCastFramework","win","_b","_a","resolve","reject","existing","onLoad","script","EXT_TO_MIME","guessContentTypeFromUrl","fileUrl","path","dot","ext","trackToMediaInfo","chromeCast","track","options","contentType","media","meta","SEEK_JUMP_SECONDS","getCastGlobals","framework","formatCastError","err","d","useGingerCast","enabled","receiverApplicationId","autoJoinPolicyOpt","resumeSavedSession","syncLocalAudio","endSessionOnUnmount","contentTypeResolver","onError","state","audioRef","useGinger","currentTrack","getCurrentTrack","frameworkReady","setFrameworkReady","useState","isAvailable","setIsAvailable","isConnected","setIsConnected","sessionState","setSessionState","error","setError","receiverCurrentTime","setReceiverCurrentTime","receiverDuration","setReceiverDuration","receiverIsPaused","setReceiverIsPaused","remotePlayerRef","useRef","remoteControllerRef","lastMediaKeyRef","prevSeekTimeRef","prevSeekIndexRef","localsRef","skipPlayPauseUntilRef","stateCurrentTimeRef","onErrorRef","emitError","useCallback","msg","useEffect","cancelled","e","globals","context","autoJoinPolicy","syncConnected","connected","onCastState","onSessionState","player","controller","bump","RemotePlayerEventType","el","session","mediaKey","loadRequest","requestSession","endSession"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export type { WindowWithCast } from './castTypes';
2
+ export { DEFAULT_MEDIA_RECEIVER_APP_ID } from './castTypes';
3
+ export { loadCastFramework } from './loadCastFramework';
4
+ export { guessContentTypeFromUrl, trackToMediaInfo } from './trackToMediaInfo';
5
+ export { useGingerCast } from './useGingerCast';
6
+ export type { UseGingerCastOptions, UseGingerCastResult } from './useGingerCast';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cast/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,254 @@
1
+ import { useState as g, useRef as T, useCallback as O, useEffect as p } from "react";
2
+ import { u as se } from "../useGinger-DKrHZ4NU.js";
3
+ import { g as ie } from "../selectors-BT3WSsKN.js";
4
+ const ae = "CC1AD845", K = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
5
+ let N = null;
6
+ function ue() {
7
+ if (!(typeof window > "u"))
8
+ return window;
9
+ }
10
+ function $() {
11
+ var e, a;
12
+ const s = ue();
13
+ return s ? (a = (e = s.cast) == null ? void 0 : e.framework) != null && a.CastContext ? Promise.resolve() : N || (N = new Promise((d, u) => {
14
+ const c = document.querySelector(`script[src="${K}"]`);
15
+ if (c) {
16
+ const S = () => {
17
+ var y, i;
18
+ (i = (y = s.cast) == null ? void 0 : y.framework) != null && i.CastContext ? d() : u(new Error("Cast script loaded but cast.framework is missing."));
19
+ };
20
+ if (c.dataset.gingerCastLoaded === "true") {
21
+ S();
22
+ return;
23
+ }
24
+ c.addEventListener("load", S, { once: !0 }), c.addEventListener("error", () => u(new Error("Cast script failed to load.")), {
25
+ once: !0
26
+ });
27
+ return;
28
+ }
29
+ const m = document.createElement("script");
30
+ m.src = K, m.async = !0, m.addEventListener("load", () => {
31
+ var S, y;
32
+ m.dataset.gingerCastLoaded = "true", (y = (S = s.cast) == null ? void 0 : S.framework) != null && y.CastContext ? d() : u(new Error("Cast script loaded but cast.framework is missing."));
33
+ }), m.addEventListener("error", () => {
34
+ u(new Error("Cast script failed to load."));
35
+ }), document.head.appendChild(m);
36
+ }), N) : Promise.reject(new Error("Cast is only available in a browser environment."));
37
+ }
38
+ const ce = {
39
+ ".mp3": "audio/mpeg",
40
+ ".aac": "audio/aac",
41
+ ".m4a": "audio/mp4",
42
+ ".ogg": "audio/ogg",
43
+ ".opus": "audio/opus",
44
+ ".wav": "audio/wav",
45
+ ".flac": "audio/flac",
46
+ ".webm": "audio/webm"
47
+ };
48
+ function le(s) {
49
+ try {
50
+ const e = new URL(s, "https://example.com").pathname.toLowerCase(), a = e.lastIndexOf(".");
51
+ if (a === -1) return "audio/mpeg";
52
+ const d = e.slice(a);
53
+ return ce[d] ?? "audio/mpeg";
54
+ } catch {
55
+ return "audio/mpeg";
56
+ }
57
+ }
58
+ function de(s, e, a) {
59
+ var m;
60
+ const d = ((m = a == null ? void 0 : a.contentTypeResolver) == null ? void 0 : m.call(a, e)) ?? le(e.fileUrl), u = new s.media.MediaInfo(e.fileUrl, d);
61
+ u.streamType = s.media.StreamType.BUFFERED;
62
+ const c = new s.media.MusicTrackMediaMetadata();
63
+ return c.metadataType = s.media.MetadataType.MUSIC_TRACK, c.title = e.title, e.artist && (c.artist = e.artist), e.album && (c.albumName = e.album), e.artworkUrl && (c.images = [{ url: e.artworkUrl }]), u.metadata = c, new s.media.LoadRequest(u);
64
+ }
65
+ const me = 2;
66
+ function P(s) {
67
+ var d, u;
68
+ const e = (d = s.cast) == null ? void 0 : d.framework, a = (u = s.chrome) == null ? void 0 : u.cast;
69
+ return !(e != null && e.CastContext) || !(a != null && a.media) ? null : {
70
+ context: e.CastContext.getInstance(),
71
+ framework: e,
72
+ chromeCast: a
73
+ };
74
+ }
75
+ function fe(s) {
76
+ if (s instanceof Error) return s.message;
77
+ if (s && typeof s == "object" && "description" in s) {
78
+ const e = s.description;
79
+ if (typeof e == "string" && e.length > 0) return e;
80
+ }
81
+ return "Unknown Cast error";
82
+ }
83
+ function ve(s = {}) {
84
+ const {
85
+ enabled: e = !0,
86
+ receiverApplicationId: a = ae,
87
+ autoJoinPolicy: d,
88
+ resumeSavedSession: u = !0,
89
+ syncLocalAudio: c = "pause-mute",
90
+ endSessionOnUnmount: m = !1,
91
+ contentTypeResolver: S,
92
+ onError: y
93
+ } = s, { state: i, audioRef: I } = se(), R = ie(i), [w, j] = g(!1), [B, V] = g(!1), [f, W] = g(!1), [X, z] = g(null), [Q, U] = g(null), [Y, Z] = g(0), [ee, te] = g(0), [ne, re] = g(!0), M = T(null), x = T(
94
+ null
95
+ ), A = T(null), D = T(0), k = T(-1), E = T(null), h = T(0), H = T(i.currentTime);
96
+ H.current = i.currentTime;
97
+ const b = T(y);
98
+ b.current = y;
99
+ const v = O((r) => {
100
+ var n;
101
+ const t = fe(r);
102
+ U(t);
103
+ try {
104
+ (n = b.current) == null || n.call(b, (r instanceof Error, r));
105
+ } catch {
106
+ }
107
+ }, []);
108
+ p(() => {
109
+ if (!e) return;
110
+ let r = !1;
111
+ return $().then(() => {
112
+ var n, o;
113
+ if (r) return;
114
+ j(!0), V(!!((o = (n = window.chrome) == null ? void 0 : n.cast) != null && o.isAvailable));
115
+ }).catch((t) => {
116
+ r || v(t instanceof Error ? t : new Error(String(t)));
117
+ }), () => {
118
+ r = !0;
119
+ };
120
+ }, [e, v]), p(() => {
121
+ var J;
122
+ if (!e || !w) return;
123
+ const t = P(window);
124
+ if (!t) return;
125
+ const { context: n, framework: o, chromeCast: _ } = t, l = d ?? ((J = _.AutoJoinPolicy) == null ? void 0 : J.ORIGIN_SCOPED) ?? 1;
126
+ n.setOptions({
127
+ receiverApplicationId: a,
128
+ autoJoinPolicy: l,
129
+ resumeSavedSession: u
130
+ });
131
+ const C = () => {
132
+ const L = n.getCastState() === o.CastState.CONNECTED;
133
+ W(L);
134
+ }, F = () => C(), q = (L) => {
135
+ L.sessionState && z(L.sessionState), C();
136
+ };
137
+ return n.addEventListener(o.CastContextEventType.CAST_STATE_CHANGED, F), n.addEventListener(o.CastContextEventType.SESSION_STATE_CHANGED, q), C(), () => {
138
+ n.removeEventListener(o.CastContextEventType.CAST_STATE_CHANGED, F), n.removeEventListener(
139
+ o.CastContextEventType.SESSION_STATE_CHANGED,
140
+ q
141
+ );
142
+ };
143
+ }, [e, w, a, d, u]), p(() => {
144
+ if (!e || !w) return;
145
+ const t = P(window);
146
+ if (!t) return;
147
+ const { framework: n } = t, o = new n.RemotePlayer(), _ = new n.RemotePlayerController(o);
148
+ M.current = o, x.current = _;
149
+ const l = () => {
150
+ Z(o.currentTime), te(o.duration), re(o.isPaused);
151
+ }, { RemotePlayerEventType: C } = n;
152
+ return o.addEventListener(C.IS_PAUSED_CHANGED, l), o.addEventListener(C.CURRENT_TIME_CHANGED, l), o.addEventListener(C.DURATION_CHANGED, l), l(), () => {
153
+ o.removeEventListener(C.IS_PAUSED_CHANGED, l), o.removeEventListener(C.CURRENT_TIME_CHANGED, l), o.removeEventListener(C.DURATION_CHANGED, l), M.current = null, x.current = null;
154
+ };
155
+ }, [e, w]), p(() => {
156
+ if (!e || c === "none") return;
157
+ const r = I.current;
158
+ if (!f || !r) {
159
+ E.current && r && (r.muted = E.current.muted, r.volume = E.current.volume, E.current = null);
160
+ return;
161
+ }
162
+ return E.current || (E.current = { volume: r.volume, muted: r.muted }), r.muted = !0, r.volume = 0, () => {
163
+ E.current && I.current && (I.current.muted = E.current.muted, I.current.volume = E.current.volume, E.current = null);
164
+ };
165
+ }, [e, c, f, I]), p(() => {
166
+ if (!e || !w || !f) return;
167
+ const t = P(window);
168
+ if (!t) return;
169
+ const n = t.context.getCurrentSession();
170
+ if (!n || !R) return;
171
+ const o = `${i.currentIndex}:${R.fileUrl}`, _ = t.chromeCast;
172
+ if (A.current === o) return;
173
+ A.current = o, h.current = Date.now() + 500;
174
+ const l = de(_, R, { contentTypeResolver: S });
175
+ l.autoplay = !i.isPaused, l.currentTime = 0, n.loadMedia(
176
+ l,
177
+ () => {
178
+ k.current = i.currentIndex, D.current = H.current;
179
+ },
180
+ (C) => v(C)
181
+ );
182
+ }, [
183
+ e,
184
+ w,
185
+ f,
186
+ i.currentIndex,
187
+ i.isPaused,
188
+ R,
189
+ S,
190
+ v
191
+ ]), p(() => {
192
+ f || (A.current = null);
193
+ }, [f]), p(() => {
194
+ if (!e || !w || !f || !R) return;
195
+ const r = `${i.currentIndex}:${R.fileUrl}`;
196
+ if (A.current !== r || Date.now() < h.current) return;
197
+ const t = M.current, n = x.current;
198
+ !t || !n || t.isPaused !== i.isPaused && n.playOrPause();
199
+ }, [e, w, f, i.isPaused, i.currentIndex, R]), p(() => {
200
+ var t;
201
+ if (!e || !f) return;
202
+ if (k.current !== i.currentIndex) {
203
+ k.current = i.currentIndex, D.current = i.currentTime;
204
+ return;
205
+ }
206
+ Math.abs(i.currentTime - D.current) > me && ((t = x.current) == null || t.seek({ value: i.currentTime })), D.current = i.currentTime;
207
+ }, [e, f, i.currentTime, i.currentIndex]);
208
+ const oe = O(async () => {
209
+ w || await $();
210
+ const t = P(window);
211
+ if (!t) {
212
+ v(new Error("Cast framework is not available."));
213
+ return;
214
+ }
215
+ try {
216
+ await t.context.requestSession(), U(null);
217
+ } catch (n) {
218
+ v(n);
219
+ }
220
+ }, [v, w]), G = O(() => {
221
+ const t = P(window), n = t == null ? void 0 : t.context.getCurrentSession();
222
+ n && n.endSession(
223
+ () => {
224
+ A.current = null, U(null);
225
+ },
226
+ (o) => v(o)
227
+ );
228
+ }, [v]);
229
+ return p(() => {
230
+ if (m)
231
+ return () => {
232
+ G();
233
+ };
234
+ }, [m, G]), {
235
+ isAvailable: B,
236
+ isConnected: f,
237
+ isCasting: f,
238
+ sessionState: X,
239
+ error: Q,
240
+ requestSession: oe,
241
+ endSession: G,
242
+ receiverCurrentTime: Y,
243
+ receiverDuration: ee,
244
+ receiverIsPaused: ne
245
+ };
246
+ }
247
+ export {
248
+ ae as DEFAULT_MEDIA_RECEIVER_APP_ID,
249
+ le as guessContentTypeFromUrl,
250
+ $ as loadCastFramework,
251
+ de as trackToMediaInfo,
252
+ ve as useGingerCast
253
+ };
254
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/cast/castTypes.ts","../../src/cast/loadCastFramework.ts","../../src/cast/trackToMediaInfo.ts","../../src/cast/useGingerCast.ts"],"sourcesContent":["/**\n * Minimal typings for the subset of the Google Cast Web Sender API used by Ginger.\n * Runtime objects come from the loaded Cast Framework script (`loadCastFramework`).\n */\n\n/** Default Media Receiver application ID (audio/video). */\nexport const DEFAULT_MEDIA_RECEIVER_APP_ID = \"CC1AD845\";\n\nexport type CastImage = {\n url: string;\n width?: number;\n height?: number;\n};\n\n/** Instance shape from `new chrome.cast.media.MusicTrackMediaMetadata()`. */\nexport type CastMusicTrackMetadata = {\n metadataType: number;\n title?: string;\n artist?: string;\n albumName?: string;\n images?: CastImage[];\n};\n\nexport type CastMediaInfoLike = {\n contentId: string;\n contentType: string;\n streamType?: string;\n metadata?: CastMusicTrackMetadata;\n};\n\nexport type CastLoadRequestLike = {\n media: CastMediaInfoLike;\n autoplay?: boolean;\n currentTime?: number;\n};\n\nexport type CastMediaInfoConstructor = new (\n contentId: string,\n contentType: string,\n) => CastMediaInfoLike;\nexport type CastMusicTrackMetadataConstructor = new () => CastMusicTrackMetadata;\nexport type CastLoadRequestConstructor = new (mediaInfo: CastMediaInfoLike) => CastLoadRequestLike;\n\n/** Subset of `chrome.cast.Session` methods we call (duck-typed). */\nexport type CastSessionLike = {\n loadMedia: (\n request: CastLoadRequestLike,\n successCallback: (media?: unknown) => void,\n errorCallback: (error: CastErrorLike) => void,\n ) => void;\n endSession: (successCallback: () => void, errorCallback: (error: CastErrorLike) => void) => void;\n};\n\nexport type CastErrorLike = {\n code?: string;\n description?: string;\n};\n\nexport type CastContextOptionsLike = {\n receiverApplicationId: string;\n autoJoinPolicy?: number;\n resumeSavedSession?: boolean;\n};\n\n/** `cast.framework.CastContext` duck type. */\nexport type CastFrameworkContextLike = {\n setOptions: (options: CastContextOptionsLike) => void;\n getCastState: () => number;\n getCurrentSession: () => CastSessionLike | null;\n addEventListener: (type: string, listener: (event: { sessionState?: string }) => void) => void;\n removeEventListener: (type: string, listener: (event: { sessionState?: string }) => void) => void;\n requestSession: () => Promise<void>;\n};\n\nexport type CastFrameworkNamespace = {\n CastContext: {\n getInstance: () => CastFrameworkContextLike;\n };\n CastContextEventType: {\n CAST_STATE_CHANGED: string;\n SESSION_STATE_CHANGED: string;\n };\n CastState: {\n NO_DEVICES: number;\n NOT_CONNECTED: number;\n CONNECTING: number;\n CONNECTED: number;\n };\n SessionState: {\n SESSION_STARTED: string;\n SESSION_ENDED: string;\n SESSION_RESUMED: string;\n };\n RemotePlayer: new () => CastRemotePlayerLike;\n RemotePlayerController: new (player: CastRemotePlayerLike) => CastRemotePlayerControllerLike;\n RemotePlayerEventType: {\n IS_PAUSED_CHANGED: string;\n CURRENT_TIME_CHANGED: string;\n DURATION_CHANGED: string;\n };\n};\n\nexport type CastRemotePlayerLike = {\n isPaused: boolean;\n currentTime: number;\n duration: number;\n controller?: CastRemotePlayerControllerLike;\n addEventListener: (type: string, handler: () => void) => void;\n removeEventListener: (type: string, handler: () => void) => void;\n};\n\nexport type CastRemotePlayerControllerLike = {\n playOrPause: () => void;\n seek: (options: { value: number }) => void;\n stop: () => void;\n};\n\nexport type ChromeCastNamespace = {\n media: {\n DEFAULT_MEDIA_RECEIVER_APP_ID: string;\n MediaInfo: CastMediaInfoConstructor;\n MusicTrackMediaMetadata: CastMusicTrackMetadataConstructor;\n LoadRequest: CastLoadRequestConstructor;\n MetadataType: {\n MUSIC_TRACK: number;\n };\n StreamType: {\n BUFFERED: string;\n };\n };\n isAvailable: boolean;\n AutoJoinPolicy: {\n ORIGIN_SCOPED: number;\n TAB_AND_ORIGIN_SCOPED: number;\n };\n};\n\nexport type WindowWithCast = Window & {\n cast?: { framework?: CastFrameworkNamespace };\n chrome?: { cast?: ChromeCastNamespace };\n};\n","import type { WindowWithCast } from \"./castTypes\";\n\nconst CAST_SCRIPT_SRC =\n \"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1\";\n\nlet loadPromise: Promise<void> | null = null;\n\nfunction getWindow(): WindowWithCast | undefined {\n if (typeof window === \"undefined\") return undefined;\n return window as WindowWithCast;\n}\n\n/**\n * Loads the Google Cast Web Sender script (Cast Application Framework) once.\n * Resolves when `window.cast.framework` is available. Safe to call multiple times.\n *\n * In SSR environments (no `window`), rejects with an error.\n */\nexport function loadCastFramework(): Promise<void> {\n const win = getWindow();\n if (!win) {\n return Promise.reject(new Error(\"Cast is only available in a browser environment.\"));\n }\n\n if (win.cast?.framework?.CastContext) {\n return Promise.resolve();\n }\n\n if (loadPromise) {\n return loadPromise;\n }\n\n loadPromise = new Promise((resolve, reject) => {\n const existing = document.querySelector(`script[src=\"${CAST_SCRIPT_SRC}\"]`);\n if (existing) {\n const onLoad = () => {\n if (win.cast?.framework?.CastContext) {\n resolve();\n } else {\n reject(new Error(\"Cast script loaded but cast.framework is missing.\"));\n }\n };\n if ((existing as HTMLScriptElement).dataset.gingerCastLoaded === \"true\") {\n onLoad();\n return;\n }\n existing.addEventListener(\"load\", onLoad, { once: true });\n existing.addEventListener(\"error\", () => reject(new Error(\"Cast script failed to load.\")), {\n once: true,\n });\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = CAST_SCRIPT_SRC;\n script.async = true;\n script.addEventListener(\"load\", () => {\n script.dataset.gingerCastLoaded = \"true\";\n if (win.cast?.framework?.CastContext) {\n resolve();\n } else {\n reject(new Error(\"Cast script loaded but cast.framework is missing.\"));\n }\n });\n script.addEventListener(\"error\", () => {\n reject(new Error(\"Cast script failed to load.\"));\n });\n document.head.appendChild(script);\n });\n\n return loadPromise;\n}\n","import type { Track } from \"../types\";\nimport type { CastLoadRequestLike, ChromeCastNamespace } from \"./castTypes\";\n\nconst EXT_TO_MIME: Record<string, string> = {\n \".mp3\": \"audio/mpeg\",\n \".aac\": \"audio/aac\",\n \".m4a\": \"audio/mp4\",\n \".ogg\": \"audio/ogg\",\n \".opus\": \"audio/opus\",\n \".wav\": \"audio/wav\",\n \".flac\": \"audio/flac\",\n \".webm\": \"audio/webm\",\n};\n\n/**\n * Guess a MIME type from a file URL path. Falls back to `audio/mpeg`.\n */\nexport function guessContentTypeFromUrl(fileUrl: string): string {\n try {\n const path = new URL(fileUrl, \"https://example.com\").pathname.toLowerCase();\n const dot = path.lastIndexOf(\".\");\n if (dot === -1) return \"audio/mpeg\";\n const ext = path.slice(dot);\n return EXT_TO_MIME[ext] ?? \"audio/mpeg\";\n } catch {\n return \"audio/mpeg\";\n }\n}\n\n/**\n * Builds a Cast `LoadRequest` for the given track using the runtime `chrome.cast.media` constructors.\n * Call only after `loadCastFramework()` and when `chrome.cast` is defined.\n */\nexport function trackToMediaInfo(\n chromeCast: ChromeCastNamespace,\n track: Track,\n options?: {\n contentTypeResolver?: (t: Track) => string;\n },\n): CastLoadRequestLike {\n const contentType =\n options?.contentTypeResolver?.(track) ?? guessContentTypeFromUrl(track.fileUrl);\n\n const media = new chromeCast.media.MediaInfo(track.fileUrl, contentType);\n media.streamType = chromeCast.media.StreamType.BUFFERED;\n\n const meta = new chromeCast.media.MusicTrackMediaMetadata();\n meta.metadataType = chromeCast.media.MetadataType.MUSIC_TRACK;\n meta.title = track.title;\n if (track.artist) {\n meta.artist = track.artist;\n }\n if (track.album) {\n meta.albumName = track.album;\n }\n if (track.artworkUrl) {\n meta.images = [{ url: track.artworkUrl }];\n }\n\n media.metadata = meta;\n\n return new chromeCast.media.LoadRequest(media);\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport { getCurrentTrack } from \"../internal/selectors\";\nimport type { Track } from \"../types\";\nimport type { CastErrorLike, WindowWithCast } from \"./castTypes\";\nimport { DEFAULT_MEDIA_RECEIVER_APP_ID } from \"./castTypes\";\nimport { loadCastFramework } from \"./loadCastFramework\";\nimport { trackToMediaInfo } from \"./trackToMediaInfo\";\n\nconst SEEK_JUMP_SECONDS = 2;\n\nfunction getCastGlobals(win: WindowWithCast): {\n context: import(\"./castTypes\").CastFrameworkContextLike;\n framework: import(\"./castTypes\").CastFrameworkNamespace;\n chromeCast: import(\"./castTypes\").ChromeCastNamespace;\n} | null {\n const framework = win.cast?.framework;\n const chromeCast = win.chrome?.cast;\n if (!framework?.CastContext || !chromeCast?.media) return null;\n return {\n context: framework.CastContext.getInstance(),\n framework,\n chromeCast,\n };\n}\n\nexport type UseGingerCastOptions = {\n /** When false, the hook does not touch Cast APIs. Default: true. */\n enabled?: boolean;\n /** Receiver app ID. Default: Default Media Receiver (`CC1AD845`). */\n receiverApplicationId?: string;\n /** Passed to `CastContext.setOptions`. Default: `chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED`. */\n autoJoinPolicy?: number;\n /** Passed to `CastContext.setOptions`. Default: true. */\n resumeSavedSession?: boolean;\n /**\n * When connected, silences the local `<audio>` (`muted` + `volume = 0`) so it does not compete\n * with the TV. Restored on disconnect. Does **not** change Ginger playback state.\n * Default: `\"pause-mute\"` (silence local element).\n */\n syncLocalAudio?: \"pause-mute\" | \"none\";\n /** When true, ends the Cast session when the hook unmounts. Default: false. */\n endSessionOnUnmount?: boolean;\n contentTypeResolver?: (track: Track) => string;\n onError?: (error: CastErrorLike | Error) => void;\n};\n\nexport type UseGingerCastResult = {\n /** Cast APIs loaded and `chrome.cast` is present (may still be `false` on unsupported browsers). */\n isAvailable: boolean;\n /** Cast sender is connected to a receiver. */\n isConnected: boolean;\n /** Same as `isConnected`; use to gate `Ginger.Player` (`{!isCasting && <Ginger.Player />}`). */\n isCasting: boolean;\n /** Last `SESSION_STATE_CHANGED` value from CAF, if any. */\n sessionState: string | null;\n error: string | null;\n /** Starts a Cast session (shows the Cast device picker when needed). */\n requestSession: () => Promise<void>;\n /** Ends the current Cast session, if any. */\n endSession: () => void;\n /** Position reported by `RemotePlayer` on the sender (useful if local audio is unmounted). */\n receiverCurrentTime: number;\n /** Duration reported by `RemotePlayer`. */\n receiverDuration: number;\n /** Pause state reported by `RemotePlayer`. */\n receiverIsPaused: boolean;\n};\n\nfunction formatCastError(err: CastErrorLike | Error | unknown): string {\n if (err instanceof Error) return err.message;\n if (err && typeof err === \"object\" && \"description\" in err) {\n const d = (err as CastErrorLike).description;\n if (typeof d === \"string\" && d.length > 0) return d;\n }\n return \"Unknown Cast error\";\n}\n\n/**\n * Chromecast Web Sender (CAF) bridge: loads the current Ginger track on a Cast receiver and keeps\n * transport roughly in sync while Ginger remains the queue source of truth.\n *\n * Prefer **`{!isCasting && <Ginger.Player />}`** so local `<audio>` does not decode the same URLs\n * as the receiver. When you must keep `Ginger.Player` mounted, use `syncLocalAudio: \"pause-mute\"`\n * to mute the local element.\n *\n * ```ts\n * import { useGingerCast } from \"@lucaismyname/ginger/cast\";\n * ```\n */\nexport function useGingerCast(options: UseGingerCastOptions = {}): UseGingerCastResult {\n const {\n enabled = true,\n receiverApplicationId = DEFAULT_MEDIA_RECEIVER_APP_ID,\n autoJoinPolicy: autoJoinPolicyOpt,\n resumeSavedSession = true,\n syncLocalAudio = \"pause-mute\",\n endSessionOnUnmount = false,\n contentTypeResolver,\n onError,\n } = options;\n\n const { state, audioRef } = useGinger();\n const currentTrack = getCurrentTrack(state);\n\n const [frameworkReady, setFrameworkReady] = useState(false);\n const [isAvailable, setIsAvailable] = useState(false);\n const [isConnected, setIsConnected] = useState(false);\n const [sessionState, setSessionState] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [receiverCurrentTime, setReceiverCurrentTime] = useState(0);\n const [receiverDuration, setReceiverDuration] = useState(0);\n const [receiverIsPaused, setReceiverIsPaused] = useState(true);\n\n const remotePlayerRef = useRef<import(\"./castTypes\").CastRemotePlayerLike | null>(null);\n const remoteControllerRef = useRef<import(\"./castTypes\").CastRemotePlayerControllerLike | null>(\n null,\n );\n const lastMediaKeyRef = useRef<string | null>(null);\n const prevSeekTimeRef = useRef(0);\n const prevSeekIndexRef = useRef<number>(-1);\n const localsRef = useRef<{ volume: number; muted: boolean } | null>(null);\n const skipPlayPauseUntilRef = useRef(0);\n const stateCurrentTimeRef = useRef(state.currentTime);\n stateCurrentTimeRef.current = state.currentTime;\n\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n\n const emitError = useCallback((err: CastErrorLike | Error | unknown) => {\n const msg = formatCastError(err);\n setError(msg);\n try {\n onErrorRef.current?.(err instanceof Error ? err : (err as CastErrorLike));\n } catch {\n /* ignore user handler errors */\n }\n }, []);\n\n // Load CAF once when enabled.\n useEffect(() => {\n if (!enabled) return;\n let cancelled = false;\n void loadCastFramework()\n .then(() => {\n if (cancelled) return;\n setFrameworkReady(true);\n const win = window as WindowWithCast;\n setIsAvailable(Boolean(win.chrome?.cast?.isAvailable));\n })\n .catch((e: unknown) => {\n if (cancelled) return;\n emitError(e instanceof Error ? e : new Error(String(e)));\n });\n return () => {\n cancelled = true;\n };\n }, [enabled, emitError]);\n\n // CastContext options + connection listeners.\n useEffect(() => {\n if (!enabled || !frameworkReady) return;\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) return;\n\n const { context, framework, chromeCast } = globals;\n const autoJoinPolicy = autoJoinPolicyOpt ?? chromeCast.AutoJoinPolicy?.ORIGIN_SCOPED ?? 1;\n\n context.setOptions({\n receiverApplicationId,\n autoJoinPolicy,\n resumeSavedSession,\n });\n\n const syncConnected = () => {\n const connected = context.getCastState() === framework.CastState.CONNECTED;\n setIsConnected(connected);\n };\n\n const onCastState = () => syncConnected();\n const onSessionState = (e: { sessionState?: string }) => {\n if (e.sessionState) setSessionState(e.sessionState);\n syncConnected();\n };\n\n context.addEventListener(framework.CastContextEventType.CAST_STATE_CHANGED, onCastState);\n context.addEventListener(framework.CastContextEventType.SESSION_STATE_CHANGED, onSessionState);\n syncConnected();\n\n return () => {\n context.removeEventListener(framework.CastContextEventType.CAST_STATE_CHANGED, onCastState);\n context.removeEventListener(\n framework.CastContextEventType.SESSION_STATE_CHANGED,\n onSessionState,\n );\n };\n }, [enabled, frameworkReady, receiverApplicationId, autoJoinPolicyOpt, resumeSavedSession]);\n\n // RemotePlayer (for seek / play/pause / progress mirrors).\n useEffect(() => {\n if (!enabled || !frameworkReady) return;\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) return;\n\n const { framework } = globals;\n const player = new framework.RemotePlayer();\n const controller = new framework.RemotePlayerController(player);\n remotePlayerRef.current = player;\n remoteControllerRef.current = controller;\n\n const bump = () => {\n setReceiverCurrentTime(player.currentTime);\n setReceiverDuration(player.duration);\n setReceiverIsPaused(player.isPaused);\n };\n\n const { RemotePlayerEventType } = framework;\n player.addEventListener(RemotePlayerEventType.IS_PAUSED_CHANGED, bump);\n player.addEventListener(RemotePlayerEventType.CURRENT_TIME_CHANGED, bump);\n player.addEventListener(RemotePlayerEventType.DURATION_CHANGED, bump);\n bump();\n\n return () => {\n player.removeEventListener(RemotePlayerEventType.IS_PAUSED_CHANGED, bump);\n player.removeEventListener(RemotePlayerEventType.CURRENT_TIME_CHANGED, bump);\n player.removeEventListener(RemotePlayerEventType.DURATION_CHANGED, bump);\n remotePlayerRef.current = null;\n remoteControllerRef.current = null;\n };\n }, [enabled, frameworkReady]);\n\n // Silence local <audio> while casting (optional).\n useEffect(() => {\n if (!enabled || syncLocalAudio === \"none\") return;\n const el = audioRef.current;\n if (!isConnected || !el) {\n if (localsRef.current && el) {\n el.muted = localsRef.current.muted;\n el.volume = localsRef.current.volume;\n localsRef.current = null;\n }\n return;\n }\n if (!localsRef.current) {\n localsRef.current = { volume: el.volume, muted: el.muted };\n }\n el.muted = true;\n el.volume = 0;\n return () => {\n if (localsRef.current && audioRef.current) {\n audioRef.current.muted = localsRef.current.muted;\n audioRef.current.volume = localsRef.current.volume;\n localsRef.current = null;\n }\n };\n }, [enabled, syncLocalAudio, isConnected, audioRef]);\n\n // Load media when the active track changes.\n useEffect(() => {\n if (!enabled || !frameworkReady || !isConnected) return;\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) return;\n const session = globals.context.getCurrentSession();\n if (!session || !currentTrack) return;\n\n const mediaKey = `${state.currentIndex}:${currentTrack.fileUrl}`;\n const chromeCast = globals.chromeCast;\n\n if (lastMediaKeyRef.current === mediaKey) return;\n\n lastMediaKeyRef.current = mediaKey;\n skipPlayPauseUntilRef.current = Date.now() + 500;\n const loadRequest = trackToMediaInfo(chromeCast, currentTrack, { contentTypeResolver });\n loadRequest.autoplay = !state.isPaused;\n loadRequest.currentTime = 0;\n\n session.loadMedia(\n loadRequest,\n () => {\n prevSeekIndexRef.current = state.currentIndex;\n prevSeekTimeRef.current = stateCurrentTimeRef.current;\n },\n (err) => emitError(err),\n );\n }, [\n enabled,\n frameworkReady,\n isConnected,\n state.currentIndex,\n state.isPaused,\n currentTrack,\n contentTypeResolver,\n emitError,\n ]);\n\n useEffect(() => {\n if (!isConnected) {\n lastMediaKeyRef.current = null;\n }\n }, [isConnected]);\n\n // Play/pause on the receiver when Ginger pause state changes (same track only).\n useEffect(() => {\n if (!enabled || !frameworkReady || !isConnected) return;\n if (!currentTrack) return;\n const mediaKey = `${state.currentIndex}:${currentTrack.fileUrl}`;\n if (lastMediaKeyRef.current !== mediaKey) return;\n if (Date.now() < skipPlayPauseUntilRef.current) return;\n\n const player = remotePlayerRef.current;\n const controller = remoteControllerRef.current;\n if (!player || !controller) return;\n if (player.isPaused !== state.isPaused) {\n controller.playOrPause();\n }\n }, [enabled, frameworkReady, isConnected, state.isPaused, state.currentIndex, currentTrack]);\n\n // Map large local time jumps to receiver seek (e.g. scrubbing).\n useEffect(() => {\n if (!enabled || !isConnected) return;\n if (prevSeekIndexRef.current !== state.currentIndex) {\n prevSeekIndexRef.current = state.currentIndex;\n prevSeekTimeRef.current = state.currentTime;\n return;\n }\n const jump = Math.abs(state.currentTime - prevSeekTimeRef.current);\n if (jump > SEEK_JUMP_SECONDS) {\n remoteControllerRef.current?.seek({ value: state.currentTime });\n }\n prevSeekTimeRef.current = state.currentTime;\n }, [enabled, isConnected, state.currentTime, state.currentIndex]);\n\n const requestSession = useCallback(async () => {\n if (!frameworkReady) {\n await loadCastFramework();\n }\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n if (!globals) {\n emitError(new Error(\"Cast framework is not available.\"));\n return;\n }\n try {\n await globals.context.requestSession();\n setError(null);\n } catch (e) {\n emitError(e);\n }\n }, [emitError, frameworkReady]);\n\n const endSession = useCallback(() => {\n const win = window as WindowWithCast;\n const globals = getCastGlobals(win);\n const session = globals?.context.getCurrentSession() as\n | import(\"./castTypes\").CastSessionLike\n | null;\n if (!session) return;\n session.endSession(\n () => {\n lastMediaKeyRef.current = null;\n setError(null);\n },\n (err) => emitError(err),\n );\n }, [emitError]);\n\n useEffect(() => {\n if (!endSessionOnUnmount) return;\n return () => {\n endSession();\n };\n }, [endSessionOnUnmount, endSession]);\n\n return {\n isAvailable,\n isConnected,\n isCasting: isConnected,\n sessionState,\n error,\n requestSession,\n endSession,\n receiverCurrentTime,\n receiverDuration,\n receiverIsPaused,\n };\n}\n"],"names":["DEFAULT_MEDIA_RECEIVER_APP_ID","CAST_SCRIPT_SRC","loadPromise","getWindow","loadCastFramework","win","_b","_a","resolve","reject","existing","onLoad","script","EXT_TO_MIME","guessContentTypeFromUrl","fileUrl","path","dot","ext","trackToMediaInfo","chromeCast","track","options","contentType","media","meta","SEEK_JUMP_SECONDS","getCastGlobals","framework","formatCastError","err","d","useGingerCast","enabled","receiverApplicationId","autoJoinPolicyOpt","resumeSavedSession","syncLocalAudio","endSessionOnUnmount","contentTypeResolver","onError","state","audioRef","useGinger","currentTrack","getCurrentTrack","frameworkReady","setFrameworkReady","useState","isAvailable","setIsAvailable","isConnected","setIsConnected","sessionState","setSessionState","error","setError","receiverCurrentTime","setReceiverCurrentTime","receiverDuration","setReceiverDuration","receiverIsPaused","setReceiverIsPaused","remotePlayerRef","useRef","remoteControllerRef","lastMediaKeyRef","prevSeekTimeRef","prevSeekIndexRef","localsRef","skipPlayPauseUntilRef","stateCurrentTimeRef","onErrorRef","emitError","useCallback","msg","useEffect","cancelled","e","globals","context","autoJoinPolicy","syncConnected","connected","onCastState","onSessionState","player","controller","bump","RemotePlayerEventType","el","session","mediaKey","loadRequest","requestSession","endSession"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Loads the Google Cast Web Sender script (Cast Application Framework) once.
3
+ * Resolves when `window.cast.framework` is available. Safe to call multiple times.
4
+ *
5
+ * In SSR environments (no `window`), rejects with an error.
6
+ */
7
+ export declare function loadCastFramework(): Promise<void>;
8
+ //# sourceMappingURL=loadCastFramework.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadCastFramework.d.ts","sourceRoot":"","sources":["../../src/cast/loadCastFramework.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqDjD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=loadCastFramework.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadCastFramework.test.d.ts","sourceRoot":"","sources":["../../src/cast/loadCastFramework.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ import { Track } from '../types';
2
+ import { CastLoadRequestLike, ChromeCastNamespace } from './castTypes';
3
+ /**
4
+ * Guess a MIME type from a file URL path. Falls back to `audio/mpeg`.
5
+ */
6
+ export declare function guessContentTypeFromUrl(fileUrl: string): string;
7
+ /**
8
+ * Builds a Cast `LoadRequest` for the given track using the runtime `chrome.cast.media` constructors.
9
+ * Call only after `loadCastFramework()` and when `chrome.cast` is defined.
10
+ */
11
+ export declare function trackToMediaInfo(chromeCast: ChromeCastNamespace, track: Track, options?: {
12
+ contentTypeResolver?: (t: Track) => string;
13
+ }): CastLoadRequestLike;
14
+ //# sourceMappingURL=trackToMediaInfo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trackToMediaInfo.d.ts","sourceRoot":"","sources":["../../src/cast/trackToMediaInfo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAa5E;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAU/D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,mBAAmB,EAC/B,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE;IACR,mBAAmB,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,MAAM,CAAC;CAC5C,GACA,mBAAmB,CAuBrB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=trackToMediaInfo.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trackToMediaInfo.test.d.ts","sourceRoot":"","sources":["../../src/cast/trackToMediaInfo.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,57 @@
1
+ import { Track } from '../types';
2
+ import { CastErrorLike } from './castTypes';
3
+ export type UseGingerCastOptions = {
4
+ /** When false, the hook does not touch Cast APIs. Default: true. */
5
+ enabled?: boolean;
6
+ /** Receiver app ID. Default: Default Media Receiver (`CC1AD845`). */
7
+ receiverApplicationId?: string;
8
+ /** Passed to `CastContext.setOptions`. Default: `chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED`. */
9
+ autoJoinPolicy?: number;
10
+ /** Passed to `CastContext.setOptions`. Default: true. */
11
+ resumeSavedSession?: boolean;
12
+ /**
13
+ * When connected, silences the local `<audio>` (`muted` + `volume = 0`) so it does not compete
14
+ * with the TV. Restored on disconnect. Does **not** change Ginger playback state.
15
+ * Default: `"pause-mute"` (silence local element).
16
+ */
17
+ syncLocalAudio?: "pause-mute" | "none";
18
+ /** When true, ends the Cast session when the hook unmounts. Default: false. */
19
+ endSessionOnUnmount?: boolean;
20
+ contentTypeResolver?: (track: Track) => string;
21
+ onError?: (error: CastErrorLike | Error) => void;
22
+ };
23
+ export type UseGingerCastResult = {
24
+ /** Cast APIs loaded and `chrome.cast` is present (may still be `false` on unsupported browsers). */
25
+ isAvailable: boolean;
26
+ /** Cast sender is connected to a receiver. */
27
+ isConnected: boolean;
28
+ /** Same as `isConnected`; use to gate `Ginger.Player` (`{!isCasting && <Ginger.Player />}`). */
29
+ isCasting: boolean;
30
+ /** Last `SESSION_STATE_CHANGED` value from CAF, if any. */
31
+ sessionState: string | null;
32
+ error: string | null;
33
+ /** Starts a Cast session (shows the Cast device picker when needed). */
34
+ requestSession: () => Promise<void>;
35
+ /** Ends the current Cast session, if any. */
36
+ endSession: () => void;
37
+ /** Position reported by `RemotePlayer` on the sender (useful if local audio is unmounted). */
38
+ receiverCurrentTime: number;
39
+ /** Duration reported by `RemotePlayer`. */
40
+ receiverDuration: number;
41
+ /** Pause state reported by `RemotePlayer`. */
42
+ receiverIsPaused: boolean;
43
+ };
44
+ /**
45
+ * Chromecast Web Sender (CAF) bridge: loads the current Ginger track on a Cast receiver and keeps
46
+ * transport roughly in sync while Ginger remains the queue source of truth.
47
+ *
48
+ * Prefer **`{!isCasting && <Ginger.Player />}`** so local `<audio>` does not decode the same URLs
49
+ * as the receiver. When you must keep `Ginger.Player` mounted, use `syncLocalAudio: "pause-mute"`
50
+ * to mute the local element.
51
+ *
52
+ * ```ts
53
+ * import { useGingerCast } from "@lucaismyname/ginger/cast";
54
+ * ```
55
+ */
56
+ export declare function useGingerCast(options?: UseGingerCastOptions): UseGingerCastResult;
57
+ //# sourceMappingURL=useGingerCast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerCast.d.ts","sourceRoot":"","sources":["../../src/cast/useGingerCast.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,aAAa,CAAC;AAsBjE,MAAM,MAAM,oBAAoB,GAAG;IACjC,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,+FAA+F;IAC/F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IACvC,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC/C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,KAAK,IAAI,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,oGAAoG;IACpG,WAAW,EAAE,OAAO,CAAC;IACrB,8CAA8C;IAC9C,WAAW,EAAE,OAAO,CAAC;IACrB,gGAAgG;IAChG,SAAS,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,wEAAwE;IACxE,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,6CAA6C;IAC7C,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,8FAA8F;IAC9F,mBAAmB,EAAE,MAAM,CAAC;IAC5B,2CAA2C;IAC3C,gBAAgB,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAWF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,mBAAmB,CA0SrF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaismyname/ginger",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "A headless react audio-player component primitive",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -59,6 +59,11 @@
59
59
  "import": "./dist/remote/index.js",
60
60
  "require": "./dist/remote/index.cjs"
61
61
  },
62
+ "./cast": {
63
+ "types": "./dist/cast/index.d.ts",
64
+ "import": "./dist/cast/index.js",
65
+ "require": "./dist/cast/index.cjs"
66
+ },
62
67
  "./crossfade": {
63
68
  "types": "./dist/crossfade/index.d.ts",
64
69
  "import": "./dist/crossfade/index.js",