@ai-matrx/media 0.6.67 → 0.7.1

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.
@@ -0,0 +1,873 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/live-audio-react.ts
22
+ var live_audio_react_exports = {};
23
+ __export(live_audio_react_exports, {
24
+ LIVE_AUDIO_CHUNK: () => LIVE_AUDIO_CHUNK,
25
+ LIVE_AUDIO_END: () => LIVE_AUDIO_END,
26
+ LiveAudioPlayer: () => LiveAudioPlayer,
27
+ MAX_OPEN_MEDIA_FRAME_SETS: () => MAX_OPEN_MEDIA_FRAME_SETS,
28
+ createLiveAudioController: () => createLiveAudioController,
29
+ createLiveAudioRegistry: () => createLiveAudioRegistry,
30
+ createMediaFrameAssembler: () => createMediaFrameAssembler,
31
+ createStreamingMp3Player: () => createStreamingMp3Player,
32
+ createStreamingPcmPlayer: () => createStreamingPcmPlayer,
33
+ isLiveAudioEvent: () => isLiveAudioEvent,
34
+ liveAudioEncodingOf: () => liveAudioEncodingOf,
35
+ mediaFrameSourceKey: () => mediaFrameSourceKey,
36
+ takeLiveAudioHandoff: () => takeLiveAudioHandoff,
37
+ useLiveAudio: () => useLiveAudio,
38
+ useLiveAudioPlayer: () => useLiveAudioPlayer
39
+ });
40
+ module.exports = __toCommonJS(live_audio_react_exports);
41
+
42
+ // src/live-audio/react/use-live-audio.ts
43
+ var import_react = require("react");
44
+
45
+ // src/live-audio/mp3-player.ts
46
+ function base64ToArrayBuffer(b64) {
47
+ const binary = atob(b64);
48
+ const bytes = new Uint8Array(binary.length);
49
+ for (let i = 0; i < binary.length; i++) {
50
+ bytes[i] = binary.charCodeAt(i);
51
+ }
52
+ return bytes.buffer;
53
+ }
54
+ function createStreamingMp3Player(options = {}) {
55
+ if (typeof window === "undefined" || typeof MediaSource === "undefined") {
56
+ console.info(
57
+ "[streaming-mp3] MediaSource is unavailable \u2014 live MP3 playback disabled; waiting for the canonical audio file"
58
+ );
59
+ return null;
60
+ }
61
+ const mimeType = options.mimeType || "audio/mpeg";
62
+ if (!MediaSource.isTypeSupported(mimeType)) {
63
+ console.info(
64
+ `[streaming-mp3] browser does not support MediaSource ${mimeType} \u2014 live playback disabled; waiting for the canonical audio file`
65
+ );
66
+ return null;
67
+ }
68
+ const mediaSource = new MediaSource();
69
+ const objectUrl = URL.createObjectURL(mediaSource);
70
+ const audio = document.createElement("audio");
71
+ audio.preload = "auto";
72
+ audio.hidden = true;
73
+ audio.setAttribute("aria-hidden", "true");
74
+ audio.src = objectUrl;
75
+ document.body.appendChild(audio);
76
+ const pending = [];
77
+ const callbacks = /* @__PURE__ */ new Set();
78
+ let sourceBuffer = null;
79
+ let streamEnded = false;
80
+ let destroyed = false;
81
+ let failed = false;
82
+ function notify() {
83
+ for (const callback of callbacks) callback();
84
+ }
85
+ function fail(message, cause) {
86
+ if (failed || destroyed) return;
87
+ failed = true;
88
+ const error = cause instanceof Error ? new Error(message, { cause }) : new Error(message);
89
+ console.error(`[streaming-mp3] ${message}`, cause ?? "");
90
+ options.onError?.(error);
91
+ notify();
92
+ }
93
+ function finishIfReady() {
94
+ if (!destroyed && !failed && streamEnded && pending.length === 0 && sourceBuffer && !sourceBuffer.updating && mediaSource.readyState === "open") {
95
+ try {
96
+ mediaSource.endOfStream();
97
+ } catch (error) {
98
+ fail("could not finalize the MediaSource stream", error);
99
+ }
100
+ }
101
+ }
102
+ function appendNext() {
103
+ if (destroyed || failed || !sourceBuffer || sourceBuffer.updating || mediaSource.readyState !== "open") {
104
+ return;
105
+ }
106
+ const next = pending.shift();
107
+ if (!next) {
108
+ finishIfReady();
109
+ return;
110
+ }
111
+ try {
112
+ sourceBuffer.appendBuffer(next);
113
+ } catch (error) {
114
+ fail("could not append an MP3 stream chunk", error);
115
+ }
116
+ }
117
+ mediaSource.addEventListener(
118
+ "sourceopen",
119
+ () => {
120
+ if (destroyed || failed || sourceBuffer) return;
121
+ try {
122
+ sourceBuffer = mediaSource.addSourceBuffer(mimeType);
123
+ sourceBuffer.mode = "sequence";
124
+ sourceBuffer.addEventListener("updateend", () => {
125
+ notify();
126
+ appendNext();
127
+ });
128
+ sourceBuffer.addEventListener("error", () => {
129
+ fail("MediaSource rejected the MP3 stream");
130
+ });
131
+ appendNext();
132
+ } catch (error) {
133
+ fail("could not initialize an MP3 SourceBuffer", error);
134
+ }
135
+ },
136
+ { once: true }
137
+ );
138
+ for (const event of ["timeupdate", "progress", "play", "pause", "ended"]) {
139
+ audio.addEventListener(event, notify);
140
+ }
141
+ audio.addEventListener("error", () => {
142
+ fail(
143
+ `browser could not decode the live MP3 stream${audio.error?.message ? `: ${audio.error.message}` : ""}`
144
+ );
145
+ });
146
+ function getBufferedMs() {
147
+ try {
148
+ const ranges = audio.buffered;
149
+ return ranges.length > 0 ? ranges.end(ranges.length - 1) * 1e3 : 0;
150
+ } catch {
151
+ return 0;
152
+ }
153
+ }
154
+ return {
155
+ enqueueBase64: (b64) => {
156
+ if (destroyed || failed || streamEnded) return;
157
+ try {
158
+ const bytes = base64ToArrayBuffer(b64);
159
+ if (bytes.byteLength === 0) return;
160
+ pending.push(bytes);
161
+ appendNext();
162
+ notify();
163
+ } catch (error) {
164
+ fail("received a malformed base64 MP3 chunk", error);
165
+ }
166
+ },
167
+ end: () => {
168
+ if (destroyed || failed) return;
169
+ streamEnded = true;
170
+ finishIfReady();
171
+ notify();
172
+ },
173
+ play: () => {
174
+ if (destroyed || failed) return;
175
+ void audio.play().catch((error) => {
176
+ fail("browser refused live MP3 playback", error);
177
+ });
178
+ },
179
+ pause: () => audio.pause(),
180
+ seekMs: (ms) => {
181
+ if (destroyed || failed) return;
182
+ const clampedMs = Math.max(0, Math.min(ms, getBufferedMs()));
183
+ audio.currentTime = clampedMs / 1e3;
184
+ notify();
185
+ },
186
+ getPositionMs: () => Number.isFinite(audio.currentTime) ? audio.currentTime * 1e3 : 0,
187
+ getBufferedMs,
188
+ isPlaying: () => !audio.paused && !audio.ended,
189
+ hasEnded: () => streamEnded,
190
+ onUpdate: (callback) => {
191
+ callbacks.add(callback);
192
+ return () => callbacks.delete(callback);
193
+ },
194
+ destroy: () => {
195
+ if (destroyed) return;
196
+ destroyed = true;
197
+ audio.pause();
198
+ pending.length = 0;
199
+ callbacks.clear();
200
+ if (sourceBuffer?.updating) {
201
+ try {
202
+ sourceBuffer.abort();
203
+ } catch {
204
+ }
205
+ }
206
+ audio.removeAttribute("src");
207
+ audio.load();
208
+ audio.remove();
209
+ URL.revokeObjectURL(objectUrl);
210
+ sourceBuffer = null;
211
+ }
212
+ };
213
+ }
214
+
215
+ // src/live-audio/pcm-player.ts
216
+ function base64ToInt16(b64) {
217
+ const binary = atob(b64);
218
+ const len = binary.length;
219
+ const bytes = new Uint8Array(len);
220
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
221
+ return new Int16Array(bytes.buffer, 0, len >> 1);
222
+ }
223
+ function createStreamingPcmPlayer(format) {
224
+ const sampleRate = format.sampleRate > 0 ? format.sampleRate : 24e3;
225
+ const channels = format.channels > 0 ? format.channels : 1;
226
+ const chunks = [];
227
+ const chunkEndFrames = [];
228
+ let totalFrames = 0;
229
+ let ctx = null;
230
+ const scheduled = [];
231
+ let nextChunkIndex = 0;
232
+ let nextPlayTime = 0;
233
+ let playing = false;
234
+ let ended = false;
235
+ let destroyed = false;
236
+ let pausedAtFrame = 0;
237
+ let anchorCtxTime = 0;
238
+ let anchorFrame = 0;
239
+ let updateTimer = null;
240
+ const updateCallbacks = /* @__PURE__ */ new Set();
241
+ function notify() {
242
+ for (const cb of updateCallbacks) {
243
+ try {
244
+ cb();
245
+ } catch {
246
+ }
247
+ }
248
+ }
249
+ function startUpdateLoop() {
250
+ if (updateTimer !== null) return;
251
+ updateTimer = setInterval(() => {
252
+ notify();
253
+ if (!playing && updateTimer !== null) {
254
+ clearInterval(updateTimer);
255
+ updateTimer = null;
256
+ }
257
+ }, 250);
258
+ }
259
+ function framesToMs(frames) {
260
+ return frames / sampleRate * 1e3;
261
+ }
262
+ function getPositionFrames() {
263
+ if (!playing || !ctx) return pausedAtFrame;
264
+ const elapsed = Math.max(0, ctx.currentTime - anchorCtxTime);
265
+ return Math.min(
266
+ anchorFrame + Math.round(elapsed * sampleRate),
267
+ totalFrames
268
+ );
269
+ }
270
+ function ensureContext() {
271
+ if (typeof window === "undefined") return null;
272
+ if (!ctx) {
273
+ const Ctor = window.AudioContext || window.webkitAudioContext;
274
+ if (!Ctor) return null;
275
+ ctx = new Ctor({ sampleRate });
276
+ }
277
+ if (ctx.state === "suspended") void ctx.resume();
278
+ return ctx;
279
+ }
280
+ function int16ToBuffer(c, pcm, fromFrame) {
281
+ const frames = pcm.length / channels - fromFrame;
282
+ if (frames <= 0) return null;
283
+ const buf = c.createBuffer(channels, frames, sampleRate);
284
+ for (let ch = 0; ch < channels; ch++) {
285
+ const data = buf.getChannelData(ch);
286
+ for (let i = 0; i < frames; i++) {
287
+ data[i] = (pcm[(fromFrame + i) * channels + ch] ?? 0) / 32768;
288
+ }
289
+ }
290
+ return buf;
291
+ }
292
+ function scheduleChunk(index, fromFrame) {
293
+ const pcm = chunks[index];
294
+ if (!ctx || !pcm) return;
295
+ const buf = int16ToBuffer(ctx, pcm, fromFrame);
296
+ if (!buf) return;
297
+ const src = ctx.createBufferSource();
298
+ src.buffer = buf;
299
+ src.connect(ctx.destination);
300
+ const now = ctx.currentTime;
301
+ const startAt = Math.max(now, nextPlayTime);
302
+ if (startAt > nextPlayTime && nextPlayTime > 0) {
303
+ anchorCtxTime += startAt - nextPlayTime;
304
+ }
305
+ src.start(startAt);
306
+ nextPlayTime = startAt + buf.duration;
307
+ scheduled.push(src);
308
+ src.onended = () => {
309
+ const i = scheduled.indexOf(src);
310
+ if (i !== -1) scheduled.splice(i, 1);
311
+ try {
312
+ src.disconnect();
313
+ } catch {
314
+ }
315
+ if (playing && scheduled.length === 0 && ended && nextChunkIndex >= chunks.length) {
316
+ pausedAtFrame = totalFrames;
317
+ playing = false;
318
+ notify();
319
+ }
320
+ };
321
+ }
322
+ function scheduleFrom(frame) {
323
+ if (!ctx) return;
324
+ let index = chunkEndFrames.findIndex((end2) => frame < end2);
325
+ if (index === -1) index = chunks.length;
326
+ const chunkStart = index === 0 ? 0 : chunkEndFrames[index - 1] ?? 0;
327
+ nextPlayTime = ctx.currentTime;
328
+ anchorCtxTime = ctx.currentTime;
329
+ anchorFrame = frame;
330
+ if (index < chunks.length) {
331
+ scheduleChunk(index, frame - chunkStart);
332
+ for (let i = index + 1; i < chunks.length; i++) scheduleChunk(i, 0);
333
+ }
334
+ nextChunkIndex = chunks.length;
335
+ }
336
+ function stopScheduled() {
337
+ for (const src of scheduled) {
338
+ try {
339
+ src.onended = null;
340
+ src.stop(0);
341
+ src.disconnect();
342
+ } catch {
343
+ }
344
+ }
345
+ scheduled.length = 0;
346
+ }
347
+ function enqueueBase64(b64) {
348
+ if (destroyed || ended) return;
349
+ let pcm;
350
+ try {
351
+ pcm = base64ToInt16(b64);
352
+ } catch {
353
+ return;
354
+ }
355
+ if (pcm.length === 0) return;
356
+ chunks.push(pcm);
357
+ totalFrames += pcm.length / channels;
358
+ chunkEndFrames.push(totalFrames);
359
+ if (playing && ctx) {
360
+ scheduleChunk(chunks.length - 1, 0);
361
+ nextChunkIndex = chunks.length;
362
+ }
363
+ notify();
364
+ }
365
+ function play() {
366
+ if (destroyed || playing) return;
367
+ const c = ensureContext();
368
+ if (!c) return;
369
+ if (ended && pausedAtFrame >= totalFrames) pausedAtFrame = 0;
370
+ playing = true;
371
+ scheduleFrom(pausedAtFrame);
372
+ startUpdateLoop();
373
+ notify();
374
+ }
375
+ function pause() {
376
+ if (!playing) return;
377
+ pausedAtFrame = getPositionFrames();
378
+ playing = false;
379
+ stopScheduled();
380
+ notify();
381
+ }
382
+ function seekMs(ms) {
383
+ const frame = Math.max(
384
+ 0,
385
+ Math.min(Math.round(ms / 1e3 * sampleRate), totalFrames)
386
+ );
387
+ if (playing) {
388
+ stopScheduled();
389
+ scheduleFrom(frame);
390
+ } else {
391
+ pausedAtFrame = frame;
392
+ }
393
+ notify();
394
+ }
395
+ function end() {
396
+ ended = true;
397
+ notify();
398
+ }
399
+ function destroy() {
400
+ if (destroyed) return;
401
+ destroyed = true;
402
+ playing = false;
403
+ stopScheduled();
404
+ if (updateTimer !== null) {
405
+ clearInterval(updateTimer);
406
+ updateTimer = null;
407
+ }
408
+ updateCallbacks.clear();
409
+ chunks.length = 0;
410
+ chunkEndFrames.length = 0;
411
+ if (ctx) {
412
+ void ctx.close().catch(() => void 0);
413
+ ctx = null;
414
+ }
415
+ }
416
+ return {
417
+ enqueueBase64,
418
+ end,
419
+ play,
420
+ pause,
421
+ seekMs,
422
+ getPositionMs: () => framesToMs(getPositionFrames()),
423
+ getBufferedMs: () => framesToMs(totalFrames),
424
+ isPlaying: () => playing,
425
+ hasEnded: () => ended,
426
+ onUpdate: (cb) => {
427
+ updateCallbacks.add(cb);
428
+ return () => updateCallbacks.delete(cb);
429
+ },
430
+ destroy
431
+ };
432
+ }
433
+
434
+ // src/live-audio/controller.ts
435
+ var LIVE_AUDIO_CHUNK = "audio_stream_chunk";
436
+ var LIVE_AUDIO_END = "audio_stream_end";
437
+ function isRecord(value) {
438
+ return typeof value === "object" && value !== null && !Array.isArray(value);
439
+ }
440
+ function isLiveAudioEvent(data) {
441
+ return isRecord(data) && (data.type === LIVE_AUDIO_CHUNK || data.type === LIVE_AUDIO_END);
442
+ }
443
+ function liveAudioEncodingOf(chunk) {
444
+ const mime = chunk.mime_type ?? "";
445
+ if (chunk.encoding === "mp3" || /(?:mpeg|mp3)/i.test(mime)) return "mp3";
446
+ if (chunk.encoding === "pcm_s16le" || /(?:l16|pcm)/i.test(mime)) return "pcm_s16le";
447
+ return null;
448
+ }
449
+ function createLiveAudioController(options) {
450
+ const makePcm = options.createPcmPlayer ?? createStreamingPcmPlayer;
451
+ const makeMp3 = options.createMp3Player ?? createStreamingMp3Player;
452
+ let player = null;
453
+ let expectedSeq = 0;
454
+ let streamId = null;
455
+ let encoding = null;
456
+ let broken = false;
457
+ function clearPlayer() {
458
+ player?.destroy();
459
+ player = null;
460
+ options.onPlayer(null);
461
+ }
462
+ function drop(reason) {
463
+ const message = `${reason} \u2014 dropping live playback; the saved file takes over when rendering finishes`;
464
+ if (options.onDrop) options.onDrop(message);
465
+ else console.warn(`[live-audio] ${message}`);
466
+ broken = true;
467
+ clearPlayer();
468
+ }
469
+ function onChunk(e) {
470
+ if (broken) return;
471
+ const codec = liveAudioEncodingOf(e);
472
+ if (!codec) {
473
+ drop(`unsupported audio stream format ${e.encoding || e.mime_type}`);
474
+ return;
475
+ }
476
+ if (streamId !== null && (streamId !== e.stream_id || encoding !== codec)) {
477
+ drop(
478
+ `audio stream identity/format changed mid-render (${streamId}/${encoding} \u2192 ${e.stream_id}/${codec})`
479
+ );
480
+ return;
481
+ }
482
+ if (e.seq !== expectedSeq) {
483
+ drop(`audio stream gap (expected seq ${expectedSeq}, got ${e.seq})`);
484
+ return;
485
+ }
486
+ streamId = e.stream_id;
487
+ encoding = codec;
488
+ expectedSeq = e.seq + 1;
489
+ if (!player) {
490
+ const created = codec === "mp3" ? makeMp3({
491
+ mimeType: e.mime_type || "audio/mpeg",
492
+ onError: (error) => drop(error.message)
493
+ }) : makePcm({
494
+ sampleRate: e.sample_rate || 24e3,
495
+ channels: e.channels || 1
496
+ });
497
+ if (!created) {
498
+ drop(`no browser player is available for ${codec}`);
499
+ return;
500
+ }
501
+ player = created;
502
+ options.onPlayer(created);
503
+ }
504
+ player.enqueueBase64(e.audio_base64);
505
+ }
506
+ return {
507
+ handle(data) {
508
+ if (!isLiveAudioEvent(data)) return false;
509
+ if (data.type === LIVE_AUDIO_CHUNK) {
510
+ onChunk(data);
511
+ return true;
512
+ }
513
+ player?.end();
514
+ options.onEnd?.(data);
515
+ return true;
516
+ },
517
+ reset() {
518
+ expectedSeq = 0;
519
+ streamId = null;
520
+ encoding = null;
521
+ broken = false;
522
+ if (player) clearPlayer();
523
+ },
524
+ destroy() {
525
+ player?.destroy();
526
+ player = null;
527
+ },
528
+ get player() {
529
+ return player;
530
+ }
531
+ };
532
+ }
533
+ function takeLiveAudioHandoff(player) {
534
+ const resume = player.isPlaying();
535
+ const timeSec = player.getPositionMs() / 1e3;
536
+ player.pause();
537
+ return timeSec > 0 ? { timeSec, resume } : null;
538
+ }
539
+
540
+ // src/live-audio/react/use-live-audio.ts
541
+ function useLiveAudio(options = {}) {
542
+ const [player, setPlayer] = (0, import_react.useState)(null);
543
+ const optionsRef = (0, import_react.useRef)(options);
544
+ optionsRef.current = options;
545
+ const controllerRef = (0, import_react.useRef)(null);
546
+ const controller = (0, import_react.useCallback)(() => {
547
+ if (!controllerRef.current) {
548
+ const current = optionsRef.current;
549
+ controllerRef.current = createLiveAudioController({
550
+ onPlayer: setPlayer,
551
+ onEnd: (event) => optionsRef.current.onEnd?.(event),
552
+ onDrop: (reason) => optionsRef.current.onDrop ? optionsRef.current.onDrop(reason) : console.warn(`[live-audio] ${reason}`),
553
+ ...current.createPcmPlayer ? { createPcmPlayer: current.createPcmPlayer } : {},
554
+ ...current.createMp3Player ? { createMp3Player: current.createMp3Player } : {}
555
+ });
556
+ }
557
+ return controllerRef.current;
558
+ }, []);
559
+ (0, import_react.useEffect)(
560
+ () => () => {
561
+ controllerRef.current?.destroy();
562
+ controllerRef.current = null;
563
+ },
564
+ []
565
+ );
566
+ const handle = (0, import_react.useCallback)((data) => controller().handle(data), [controller]);
567
+ const reset = (0, import_react.useCallback)(() => controller().reset(), [controller]);
568
+ return { player, handle, reset };
569
+ }
570
+
571
+ // src/live-audio/react/use-live-audio-player.ts
572
+ var import_react2 = require("react");
573
+ function useLiveAudioPlayer(registry, key) {
574
+ const subscribe = (0, import_react2.useCallback)(
575
+ (listener) => registry ? registry.subscribe(listener) : () => void 0,
576
+ [registry]
577
+ );
578
+ const read = (0, import_react2.useCallback)(
579
+ () => registry && key ? registry.player(key) : null,
580
+ [registry, key]
581
+ );
582
+ return (0, import_react2.useSyncExternalStore)(subscribe, read, () => null);
583
+ }
584
+
585
+ // src/live-audio/react/live-audio-player.tsx
586
+ var import_react3 = require("react");
587
+ var import_format = require("@ai-matrx/kit/format");
588
+
589
+ // src/react/cn.ts
590
+ var import_tailwind_merge = require("tailwind-merge");
591
+ function cn(...inputs) {
592
+ return (0, import_tailwind_merge.twMerge)(inputs.filter(Boolean).join(" "));
593
+ }
594
+
595
+ // src/react/icons.tsx
596
+ var import_jsx_runtime = require("react/jsx-runtime");
597
+ function svgProps(props) {
598
+ return {
599
+ xmlns: "http://www.w3.org/2000/svg",
600
+ viewBox: "0 0 24 24",
601
+ width: 24,
602
+ height: 24,
603
+ fill: "none",
604
+ stroke: "currentColor",
605
+ strokeWidth: 2,
606
+ strokeLinecap: "round",
607
+ strokeLinejoin: "round",
608
+ "aria-hidden": true,
609
+ ...props
610
+ };
611
+ }
612
+ function PauseIcon(props) {
613
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...svgProps(props), children: [
614
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "14", y: "3", width: "5", height: "18", rx: "1" }),
615
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "5", y: "3", width: "5", height: "18", rx: "1" })
616
+ ] });
617
+ }
618
+ function PlayIcon(props) {
619
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { ...svgProps(props), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" }) });
620
+ }
621
+
622
+ // src/live-audio/react/live-audio-player.tsx
623
+ var import_jsx_runtime2 = require("react/jsx-runtime");
624
+ function readPlayer(player) {
625
+ return {
626
+ positionMs: player.getPositionMs(),
627
+ bufferedMs: player.getBufferedMs(),
628
+ playing: player.isPlaying(),
629
+ ended: player.hasEnded()
630
+ };
631
+ }
632
+ function LiveAudioPlayer({ player, title, className }) {
633
+ const [snapshot, setSnapshot] = (0, import_react3.useState)(() => readPlayer(player));
634
+ (0, import_react3.useEffect)(() => {
635
+ const update = () => setSnapshot(readPlayer(player));
636
+ update();
637
+ const unsubscribe = player.onUpdate(update);
638
+ const timer = window.setInterval(update, 250);
639
+ return () => {
640
+ window.clearInterval(timer);
641
+ unsubscribe();
642
+ };
643
+ }, [player]);
644
+ const { positionMs, bufferedMs, playing, ended } = snapshot;
645
+ const progress = bufferedMs > 0 ? positionMs / bufferedMs * 100 : 0;
646
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: cn("rounded-2xl border border-border bg-card p-5 shadow-sm", className), children: [
647
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mb-3 flex items-center justify-between gap-2", children: [
648
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-primary", children: [
649
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "relative flex h-1.5 w-1.5", children: [
650
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary/60" }),
651
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-primary" })
652
+ ] }),
653
+ "Listen live"
654
+ ] }),
655
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-xs text-muted-foreground", children: "You're hearing the audio as it's rendered" })
656
+ ] }),
657
+ title && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mb-3 truncate text-sm font-semibold text-foreground", children: title }),
658
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-4", children: [
659
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
660
+ "button",
661
+ {
662
+ type: "button",
663
+ onClick: () => playing ? player.pause() : player.play(),
664
+ className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-colors hover:bg-primary/90 active:scale-95",
665
+ "aria-label": playing ? "Pause" : "Play",
666
+ children: playing ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PauseIcon, { className: "h-5 w-5" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PlayIcon, { className: "ml-0.5 h-5 w-5" })
667
+ }
668
+ ),
669
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0 flex-1", children: [
670
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
671
+ "div",
672
+ {
673
+ className: "relative h-2 w-full cursor-pointer overflow-hidden rounded-full bg-muted",
674
+ onClick: (e) => {
675
+ if (!bufferedMs) return;
676
+ const rect = e.currentTarget.getBoundingClientRect();
677
+ const ratio = (e.clientX - rect.left) / rect.width;
678
+ player.seekMs(ratio * bufferedMs);
679
+ },
680
+ role: "progressbar",
681
+ "aria-valuenow": Math.round(progress),
682
+ "aria-valuemin": 0,
683
+ "aria-valuemax": 100,
684
+ "aria-label": "Live audio position",
685
+ children: [
686
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
687
+ "div",
688
+ {
689
+ className: "absolute inset-y-0 left-0 rounded-full bg-primary transition-[width] duration-200",
690
+ style: { width: `${progress}%` }
691
+ }
692
+ ),
693
+ !ended && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute inset-y-0 right-0 w-4 animate-pulse rounded-r-full bg-primary/20" })
694
+ ]
695
+ }
696
+ ),
697
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-1.5 flex justify-between text-xs tabular-nums text-muted-foreground", children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: (0, import_format.formatDurationMs)(positionMs, { style: "clock" }) }),
699
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
700
+ (0, import_format.formatDurationMs)(bufferedMs, { style: "clock" }),
701
+ !ended && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "ml-1 text-muted-foreground/60", children: "rendered" })
702
+ ] })
703
+ ] })
704
+ ] })
705
+ ] })
706
+ ] });
707
+ }
708
+
709
+ // src/live-audio/registry.ts
710
+ function createLiveAudioRegistry(options = {}) {
711
+ const controllers = /* @__PURE__ */ new Map();
712
+ const players = /* @__PURE__ */ new Map();
713
+ const listeners = /* @__PURE__ */ new Set();
714
+ const notify = () => {
715
+ for (const listener of listeners) {
716
+ try {
717
+ listener();
718
+ } catch {
719
+ }
720
+ }
721
+ };
722
+ const controllerFor = (key) => {
723
+ let controller = controllers.get(key);
724
+ if (!controller) {
725
+ controller = createLiveAudioController({
726
+ onPlayer: (player) => {
727
+ if (player) players.set(key, player);
728
+ else players.delete(key);
729
+ notify();
730
+ },
731
+ onEnd: (event) => options.onEnd?.(key, event),
732
+ onDrop: (reason) => options.onDrop ? options.onDrop(key, reason) : console.warn(`[live-audio] ${key}: ${reason}`),
733
+ ...options.createPcmPlayer ? { createPcmPlayer: options.createPcmPlayer } : {},
734
+ ...options.createMp3Player ? { createMp3Player: options.createMp3Player } : {}
735
+ });
736
+ controllers.set(key, controller);
737
+ }
738
+ return controller;
739
+ };
740
+ return {
741
+ handle(key, payload) {
742
+ if (!isLiveAudioEvent(payload)) return false;
743
+ return controllerFor(key).handle(payload);
744
+ },
745
+ player: (key) => players.get(key) ?? null,
746
+ subscribe(listener) {
747
+ listeners.add(listener);
748
+ return () => listeners.delete(listener);
749
+ },
750
+ reset(key) {
751
+ controllers.get(key)?.reset();
752
+ },
753
+ destroy() {
754
+ for (const controller of controllers.values()) controller.destroy();
755
+ controllers.clear();
756
+ const had = players.size > 0;
757
+ players.clear();
758
+ if (had) notify();
759
+ }
760
+ };
761
+ }
762
+
763
+ // src/live-audio/media-frames.ts
764
+ function mediaFrameSourceKey(source) {
765
+ return `${source.nodeId ?? ""}|${source.dispatchId ?? ""}|${source.itemIndex ?? ""}`;
766
+ }
767
+ var MAX_OPEN_MEDIA_FRAME_SETS = 32;
768
+ function isRecord2(value) {
769
+ return typeof value === "object" && value !== null && !Array.isArray(value);
770
+ }
771
+ function createMediaFrameAssembler(options) {
772
+ const open = /* @__PURE__ */ new Map();
773
+ const ready = [];
774
+ let arrival = 0;
775
+ const drop = (frameId, reason) => {
776
+ open.delete(frameId);
777
+ if (options.onDrop) options.onDrop(frameId, reason);
778
+ else console.warn(`[media-frames] dropped ${frameId}: ${reason}`);
779
+ };
780
+ const release = () => {
781
+ if (ready.length === 0) return;
782
+ ready.sort((a, b) => a.streamSeq - b.streamSeq);
783
+ const blocking = /* @__PURE__ */ new Map();
784
+ for (const set of open.values()) {
785
+ blocking.set(set.key, Math.min(blocking.get(set.key) ?? Infinity, set.firstSeq));
786
+ }
787
+ for (let i = 0; i < ready.length; ) {
788
+ const item = ready[i];
789
+ const key = mediaFrameSourceKey(item.source);
790
+ if (item.streamSeq < (blocking.get(key) ?? Infinity)) {
791
+ ready.splice(i, 1);
792
+ options.onPayload(item);
793
+ } else {
794
+ i += 1;
795
+ }
796
+ }
797
+ };
798
+ return {
799
+ push(frame) {
800
+ if (frame.kind !== "media") return false;
801
+ const frameId = frame.frame_id;
802
+ const count = frame.frame_count ?? 1;
803
+ const index = frame.frame_index ?? 0;
804
+ if (!frameId || !Number.isInteger(count) || count < 1 || !Number.isInteger(index) || index < 0 || index >= count) {
805
+ if (options.onDrop) options.onDrop(frameId ?? "?", "malformed frame");
806
+ else console.warn("[media-frames] malformed media frame dropped");
807
+ return true;
808
+ }
809
+ arrival += 1;
810
+ const seq = typeof frame.stream_seq === "number" && Number.isFinite(frame.stream_seq) ? frame.stream_seq : arrival;
811
+ let set = open.get(frameId);
812
+ if (!set) {
813
+ if (open.size >= MAX_OPEN_MEDIA_FRAME_SETS) {
814
+ const oldest = open.keys().next();
815
+ if (!oldest.done) drop(oldest.value, "abandoned (too many open sets)");
816
+ }
817
+ const source = {
818
+ nodeId: frame.node_id ?? null,
819
+ dispatchId: frame.dispatch_id ?? null,
820
+ itemIndex: frame.item_index ?? null
821
+ };
822
+ set = {
823
+ key: mediaFrameSourceKey(source),
824
+ count,
825
+ slices: /* @__PURE__ */ new Map(),
826
+ firstSeq: seq,
827
+ source
828
+ };
829
+ open.set(frameId, set);
830
+ }
831
+ set.slices.set(index, frame.delta);
832
+ if (index === 0) set.firstSeq = Math.min(set.firstSeq, seq);
833
+ if (set.slices.size < set.count) {
834
+ release();
835
+ return true;
836
+ }
837
+ open.delete(frameId);
838
+ let body = "";
839
+ for (let i = 0; i < set.count; i++) body += set.slices.get(i) ?? "";
840
+ let parsed;
841
+ try {
842
+ parsed = JSON.parse(body);
843
+ } catch {
844
+ if (options.onDrop) options.onDrop(frameId, "unparseable payload");
845
+ else console.warn(`[media-frames] dropped ${frameId}: unparseable payload`);
846
+ release();
847
+ return true;
848
+ }
849
+ if (!isRecord2(parsed) || typeof parsed.type !== "string") {
850
+ if (options.onDrop) options.onDrop(frameId, "payload has no type");
851
+ else console.warn(`[media-frames] dropped ${frameId}: payload has no type`);
852
+ release();
853
+ return true;
854
+ }
855
+ for (const [id, other] of [...open.entries()]) {
856
+ if (other.key === set.key && other.firstSeq < set.firstSeq) {
857
+ drop(id, "incomplete set");
858
+ }
859
+ }
860
+ ready.push({ payload: parsed, source: set.source, streamSeq: set.firstSeq });
861
+ release();
862
+ return true;
863
+ },
864
+ flush() {
865
+ for (const id of [...open.keys()]) drop(id, "incomplete at stream end");
866
+ release();
867
+ },
868
+ get openCount() {
869
+ return open.size;
870
+ }
871
+ };
872
+ }
873
+ //# sourceMappingURL=live-audio-react.cjs.map