@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 +289 -55
- package/dist/cast/castTypes.d.ts +130 -0
- package/dist/cast/castTypes.d.ts.map +1 -0
- package/dist/cast/index.cjs +2 -0
- package/dist/cast/index.cjs.map +1 -0
- package/dist/cast/index.d.ts +7 -0
- package/dist/cast/index.d.ts.map +1 -0
- package/dist/cast/index.js +254 -0
- package/dist/cast/index.js.map +1 -0
- package/dist/cast/loadCastFramework.d.ts +8 -0
- package/dist/cast/loadCastFramework.d.ts.map +1 -0
- package/dist/cast/loadCastFramework.test.d.ts +2 -0
- package/dist/cast/loadCastFramework.test.d.ts.map +1 -0
- package/dist/cast/trackToMediaInfo.d.ts +14 -0
- package/dist/cast/trackToMediaInfo.d.ts.map +1 -0
- package/dist/cast/trackToMediaInfo.test.d.ts +2 -0
- package/dist/cast/trackToMediaInfo.test.d.ts.map +1 -0
- package/dist/cast/useGingerCast.d.ts +57 -0
- package/dist/cast/useGingerCast.d.ts.map +1 -0
- package/package.json +6 -1
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
+
#### <a id="subpath-starter-transcript"></a> `@lucaismyname/ginger/transcript`
|
|
151
256
|
|
|
152
257
|
```tsx
|
|
153
|
-
import {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 {
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
+
#### <a id="subpath-starter-cast"></a> `@lucaismyname/ginger/cast`
|
|
210
326
|
|
|
211
|
-
|
|
327
|
+
Cast needs **HTTPS** in production; use a real HTTPS `fileUrl` the receiver can fetch.
|
|
212
328
|
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
397
|
+
#### <a id="subpath-starter-experimental-gapless"></a> `@lucaismyname/ginger/experimental-gapless`
|
|
236
398
|
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
+
const tracks = [{ title: "Demo", fileUrl: "/your-audio.mp3" }];
|
|
441
|
+
|
|
442
|
+
export function App() {
|
|
245
443
|
return (
|
|
246
444
|
<>
|
|
247
|
-
<Ginger.Provider debugLabel="Main
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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
|
|
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 @@
|
|
|
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 @@
|
|
|
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.
|
|
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",
|