@tanstack/ai-client 0.22.0 → 0.23.0
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 +15 -1
- package/dist/esm/audio-recorder.js +190 -213
- package/dist/esm/audio-recorder.js.map +1 -1
- package/dist/esm/chat-client.d.ts +172 -3
- package/dist/esm/chat-client.js +1656 -1386
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/cleared-stream-tracker.d.ts +23 -0
- package/dist/esm/cleared-stream-tracker.js +97 -0
- package/dist/esm/cleared-stream-tracker.js.map +1 -0
- package/dist/esm/client-persistor.d.ts +25 -12
- package/dist/esm/client-persistor.js +260 -235
- package/dist/esm/client-persistor.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +231 -10
- package/dist/esm/connection-adapters.js +989 -574
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +1 -0
- package/dist/esm/devtools-noop.js +79 -139
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +31 -1
- package/dist/esm/devtools.js +977 -1127
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/events.js +224 -226
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +145 -2
- package/dist/esm/generation-client.js +659 -321
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-reconstruct.d.ts +21 -0
- package/dist/esm/generation-reconstruct.js +85 -0
- package/dist/esm/generation-reconstruct.js.map +1 -0
- package/dist/esm/generation-types.d.ts +289 -3
- package/dist/esm/generation-types.js +356 -13
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +9 -4
- package/dist/esm/index.js +7 -39
- package/dist/esm/interrupt-manager.d.ts +77 -0
- package/dist/esm/interrupt-manager.js +787 -0
- package/dist/esm/interrupt-manager.js.map +1 -0
- package/dist/esm/mcp-app-bridge.js +56 -64
- package/dist/esm/mcp-app-bridge.js.map +1 -1
- package/dist/esm/realtime-client.js +366 -440
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/response-stream.js +19 -26
- package/dist/esm/response-stream.js.map +1 -1
- package/dist/esm/sse-parser.js +44 -47
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/sse-utils.js +8 -9
- package/dist/esm/sse-utils.js.map +1 -1
- package/dist/esm/storage-adapters.d.ts +62 -0
- package/dist/esm/storage-adapters.js +174 -0
- package/dist/esm/storage-adapters.js.map +1 -0
- package/dist/esm/types.d.ts +212 -10
- package/dist/esm/types.js +38 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +113 -2
- package/dist/esm/video-generation-client.js +665 -379
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -7
- package/src/chat-client.ts +1079 -61
- package/src/cleared-stream-tracker.ts +151 -0
- package/src/client-persistor.ts +102 -33
- package/src/connection-adapters.ts +1185 -142
- package/src/devtools-noop.ts +4 -3
- package/src/devtools.ts +121 -3
- package/src/generation-client.ts +563 -13
- package/src/generation-reconstruct.ts +121 -0
- package/src/generation-types.ts +727 -3
- package/src/index.ts +56 -1
- package/src/interrupt-manager.ts +1440 -0
- package/src/storage-adapters.ts +242 -0
- package/src/types.ts +301 -9
- package/src/video-generation-client.ts +479 -13
- package/dist/esm/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<
|
|
2
|
+
<picture>
|
|
3
|
+
<source
|
|
4
|
+
media="(prefers-color-scheme: dark)"
|
|
5
|
+
srcset="https://tanstack.com/api/readme/ai.png?theme=dark"
|
|
6
|
+
/>
|
|
7
|
+
<source
|
|
8
|
+
media="(prefers-color-scheme: light)"
|
|
9
|
+
srcset="https://tanstack.com/api/readme/ai.png"
|
|
10
|
+
/>
|
|
11
|
+
<img
|
|
12
|
+
src="https://tanstack.com/api/readme/ai.png"
|
|
13
|
+
alt="TanStack AI"
|
|
14
|
+
width="900"
|
|
15
|
+
/>
|
|
16
|
+
</picture>
|
|
3
17
|
</div>
|
|
4
18
|
|
|
5
19
|
<br />
|
|
@@ -1,215 +1,192 @@
|
|
|
1
1
|
import { arrayBufferToBase64 } from "@tanstack/ai-utils";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
/** Invoke the user onError callback, isolating a throw so it can't disrupt
|
|
190
|
-
* internal state teardown or strand a pending promise. */
|
|
191
|
-
notifyError(error) {
|
|
192
|
-
try {
|
|
193
|
-
this.options.onError?.(error);
|
|
194
|
-
} catch {
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
releaseStream() {
|
|
198
|
-
this.stream?.getTracks().forEach((t) => t.stop());
|
|
199
|
-
this.stream = null;
|
|
200
|
-
}
|
|
201
|
-
/** Detach all event handlers so a stale recorder can't mutate our state. */
|
|
202
|
-
detachRecorder() {
|
|
203
|
-
const recorder = this.recorder;
|
|
204
|
-
if (!recorder) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
recorder.onstop = null;
|
|
208
|
-
recorder.onerror = null;
|
|
209
|
-
recorder.ondataavailable = null;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
export {
|
|
213
|
-
AudioRecorder
|
|
2
|
+
//#region src/audio-recorder.ts
|
|
3
|
+
/**
|
|
4
|
+
* Framework-agnostic browser audio recorder. Wraps `getUserMedia` +
|
|
5
|
+
* `MediaRecorder`, returns the recorder's native output (no transcode), and
|
|
6
|
+
* builds a plug-and-play {@link AudioRecording.part}.
|
|
7
|
+
*/
|
|
8
|
+
var AudioRecorder = class {
|
|
9
|
+
options;
|
|
10
|
+
recorder = null;
|
|
11
|
+
stream = null;
|
|
12
|
+
chunks = [];
|
|
13
|
+
startedAt = 0;
|
|
14
|
+
_state = "idle";
|
|
15
|
+
starting = false;
|
|
16
|
+
pendingCancel = false;
|
|
17
|
+
listeners = /* @__PURE__ */ new Set();
|
|
18
|
+
stopResolve = null;
|
|
19
|
+
stopReject = null;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
/** Feature-detect the browser media APIs. SSR/Worker-safe. */
|
|
24
|
+
static isSupported() {
|
|
25
|
+
return typeof navigator !== "undefined" && typeof navigator.mediaDevices !== "undefined" && typeof navigator.mediaDevices.getUserMedia === "function" && typeof MediaRecorder !== "undefined";
|
|
26
|
+
}
|
|
27
|
+
get state() {
|
|
28
|
+
return this._state;
|
|
29
|
+
}
|
|
30
|
+
subscribe(cb) {
|
|
31
|
+
this.listeners.add(cb);
|
|
32
|
+
return () => {
|
|
33
|
+
this.listeners.delete(cb);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
setState(state) {
|
|
37
|
+
this._state = state;
|
|
38
|
+
for (const cb of this.listeners) cb(state);
|
|
39
|
+
}
|
|
40
|
+
async start() {
|
|
41
|
+
if (this._state !== "idle" || this.starting) return;
|
|
42
|
+
this.starting = true;
|
|
43
|
+
try {
|
|
44
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: this.options.audio ?? true });
|
|
45
|
+
if (this.pendingCancel) {
|
|
46
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.stream = stream;
|
|
50
|
+
const wanted = this.options.mimeType;
|
|
51
|
+
const useMimeType = wanted && typeof MediaRecorder.isTypeSupported === "function" && MediaRecorder.isTypeSupported(wanted) ? wanted : void 0;
|
|
52
|
+
const recorder = useMimeType ? new MediaRecorder(stream, { mimeType: useMimeType }) : new MediaRecorder(stream);
|
|
53
|
+
this.chunks = [];
|
|
54
|
+
recorder.ondataavailable = (e) => {
|
|
55
|
+
if (e.data.size > 0) this.chunks.push(e.data);
|
|
56
|
+
};
|
|
57
|
+
recorder.onstop = () => {
|
|
58
|
+
this.finalize();
|
|
59
|
+
};
|
|
60
|
+
recorder.onerror = (event) => {
|
|
61
|
+
const detail = "error" in event && event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Audio recording failed");
|
|
62
|
+
this.handleError(detail);
|
|
63
|
+
};
|
|
64
|
+
this.recorder = recorder;
|
|
65
|
+
this.startedAt = Date.now();
|
|
66
|
+
recorder.start();
|
|
67
|
+
this.setState("recording");
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this.releaseStream();
|
|
70
|
+
this.recorder = null;
|
|
71
|
+
const error = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to start recording");
|
|
72
|
+
this.setState("idle");
|
|
73
|
+
this.notifyError(error);
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
this.starting = false;
|
|
77
|
+
this.pendingCancel = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
stop() {
|
|
81
|
+
if (this._state !== "recording" || !this.recorder) return Promise.reject(/* @__PURE__ */ new Error("AudioRecorder.stop() called while not recording"));
|
|
82
|
+
this.setState("stopping");
|
|
83
|
+
const recorder = this.recorder;
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const watchdog = setTimeout(() => {
|
|
86
|
+
if (this._state !== "stopping") return;
|
|
87
|
+
this.detachRecorder();
|
|
88
|
+
if (this.chunks.length > 0) this.finalize();
|
|
89
|
+
else this.handleError(/* @__PURE__ */ new Error("Recording stop timed out after 10s with no audio"));
|
|
90
|
+
}, 1e4);
|
|
91
|
+
this.stopResolve = (rec) => {
|
|
92
|
+
clearTimeout(watchdog);
|
|
93
|
+
resolve(rec);
|
|
94
|
+
};
|
|
95
|
+
this.stopReject = (err) => {
|
|
96
|
+
clearTimeout(watchdog);
|
|
97
|
+
reject(err);
|
|
98
|
+
};
|
|
99
|
+
recorder.stop();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
cancel() {
|
|
103
|
+
if (this.starting) {
|
|
104
|
+
this.pendingCancel = true;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (this._state === "idle") return;
|
|
108
|
+
const recorder = this.recorder;
|
|
109
|
+
if (recorder) {
|
|
110
|
+
this.detachRecorder();
|
|
111
|
+
try {
|
|
112
|
+
recorder.stop();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (!(err instanceof DOMException && err.name === "InvalidStateError")) this.notifyError(err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to stop recorder"));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.releaseStream();
|
|
118
|
+
this.recorder = null;
|
|
119
|
+
this.chunks = [];
|
|
120
|
+
const reject = this.stopReject;
|
|
121
|
+
this.stopResolve = null;
|
|
122
|
+
this.stopReject = null;
|
|
123
|
+
this.setState("idle");
|
|
124
|
+
reject?.(/* @__PURE__ */ new Error("Recording cancelled"));
|
|
125
|
+
}
|
|
126
|
+
async finalize() {
|
|
127
|
+
const mimeType = this.recorder?.mimeType || "audio/webm";
|
|
128
|
+
const durationMs = Date.now() - this.startedAt;
|
|
129
|
+
try {
|
|
130
|
+
const blob = new Blob(this.chunks, { type: mimeType });
|
|
131
|
+
const base64 = arrayBufferToBase64(await blob.arrayBuffer());
|
|
132
|
+
const recording = {
|
|
133
|
+
blob,
|
|
134
|
+
base64,
|
|
135
|
+
mimeType,
|
|
136
|
+
durationMs,
|
|
137
|
+
part: {
|
|
138
|
+
type: "audio",
|
|
139
|
+
source: {
|
|
140
|
+
type: "data",
|
|
141
|
+
value: base64,
|
|
142
|
+
mimeType
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
this.releaseStream();
|
|
147
|
+
this.recorder = null;
|
|
148
|
+
this.chunks = [];
|
|
149
|
+
const resolve = this.stopResolve;
|
|
150
|
+
this.stopResolve = null;
|
|
151
|
+
this.stopReject = null;
|
|
152
|
+
this.setState("idle");
|
|
153
|
+
resolve?.(recording);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
this.handleError(err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to finalize recording"));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
handleError(error) {
|
|
159
|
+
this.releaseStream();
|
|
160
|
+
this.recorder = null;
|
|
161
|
+
this.chunks = [];
|
|
162
|
+
const reject = this.stopReject;
|
|
163
|
+
this.stopResolve = null;
|
|
164
|
+
this.stopReject = null;
|
|
165
|
+
this.setState("idle");
|
|
166
|
+
reject?.(error);
|
|
167
|
+
this.notifyError(error);
|
|
168
|
+
}
|
|
169
|
+
/** Invoke the user onError callback, isolating a throw so it can't disrupt
|
|
170
|
+
* internal state teardown or strand a pending promise. */
|
|
171
|
+
notifyError(error) {
|
|
172
|
+
try {
|
|
173
|
+
this.options.onError?.(error);
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
releaseStream() {
|
|
177
|
+
this.stream?.getTracks().forEach((t) => t.stop());
|
|
178
|
+
this.stream = null;
|
|
179
|
+
}
|
|
180
|
+
/** Detach all event handlers so a stale recorder can't mutate our state. */
|
|
181
|
+
detachRecorder() {
|
|
182
|
+
const recorder = this.recorder;
|
|
183
|
+
if (!recorder) return;
|
|
184
|
+
recorder.onstop = null;
|
|
185
|
+
recorder.onerror = null;
|
|
186
|
+
recorder.ondataavailable = null;
|
|
187
|
+
}
|
|
214
188
|
};
|
|
215
|
-
//#
|
|
189
|
+
//#endregion
|
|
190
|
+
export { AudioRecorder };
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=audio-recorder.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-recorder.js","sources":["../../src/audio-recorder.ts"],"sourcesContent":["import { arrayBufferToBase64 } from '@tanstack/ai-utils'\nimport type { AudioPart } from '@tanstack/ai/client'\n\n/** Lifecycle state of an {@link AudioRecorder}. */\nexport type AudioRecorderState = 'idle' | 'recording' | 'stopping'\n\nexport interface AudioRecorderOptions {\n /** Constraints forwarded to `getUserMedia({ audio })`. Defaults to `true`. */\n audio?: MediaTrackConstraints | boolean\n /**\n * Preferred recorder mime type. Used only when\n * `MediaRecorder.isTypeSupported` reports it; otherwise the browser default\n * is used.\n */\n mimeType?: string\n /** Fired on `getUserMedia` rejection (permission denied) or recorder error. */\n onError?: (error: Error) => void\n}\n\n/**\n * Resolves the value `stop()` produces from a recorder transform callback.\n *\n * - If the callback returns a value — sync or async — that value's awaited type\n * is used (a returned `null` is a real value and is preserved).\n * - If the callback returns nothing (`void`/`undefined`), or is absent, falls\n * back to {@link AudioRecording}.\n *\n * @template TFn - The transform callback type (or undefined if not provided)\n */\nexport type InferAudioRecordingOutput<TFn> = TFn extends (\n recording: AudioRecording,\n) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? AudioRecording\n : Exclude<Awaited<R>, void | undefined>\n : AudioRecording\n\nexport interface AudioRecording {\n /** The raw recorded media blob. */\n blob: Blob\n /** Base64 of the recorded bytes (no `data:` prefix). */\n base64: string\n /** The recorder's native mime type, e.g. `audio/webm;codecs=opus`. */\n mimeType: string\n /** Recording length in milliseconds. */\n durationMs: number\n /**\n * Ready-to-use audio content part for `sendMessage`/generation prompts:\n * `{ type: 'audio', source: { type: 'data', value: base64, mimeType } }`.\n */\n part: AudioPart\n}\n\n/**\n * Framework-agnostic browser audio recorder. Wraps `getUserMedia` +\n * `MediaRecorder`, returns the recorder's native output (no transcode), and\n * builds a plug-and-play {@link AudioRecording.part}.\n */\nexport class AudioRecorder {\n private readonly options: AudioRecorderOptions\n private recorder: MediaRecorder | null = null\n private stream: MediaStream | null = null\n private chunks: Array<Blob> = []\n private startedAt = 0\n private _state: AudioRecorderState = 'idle'\n // True while start() is awaiting getUserMedia (state is still 'idle' then).\n private starting = false\n // Set by cancel()/teardown during that window so start() releases the\n // freshly acquired stream instead of beginning a leaked recording.\n private pendingCancel = false\n private readonly listeners = new Set<(state: AudioRecorderState) => void>()\n private stopResolve: ((recording: AudioRecording) => void) | null = null\n private stopReject: ((error: Error) => void) | null = null\n\n constructor(options: AudioRecorderOptions = {}) {\n this.options = options\n }\n\n /** Feature-detect the browser media APIs. SSR/Worker-safe. */\n static isSupported(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n typeof navigator.mediaDevices !== 'undefined' &&\n typeof navigator.mediaDevices.getUserMedia === 'function' &&\n typeof MediaRecorder !== 'undefined'\n )\n }\n\n get state(): AudioRecorderState {\n return this._state\n }\n\n subscribe(cb: (state: AudioRecorderState) => void): () => void {\n this.listeners.add(cb)\n return () => {\n this.listeners.delete(cb)\n }\n }\n\n private setState(state: AudioRecorderState): void {\n this._state = state\n for (const cb of this.listeners) {\n cb(state)\n }\n }\n\n async start(): Promise<void> {\n if (this._state !== 'idle' || this.starting) {\n return\n }\n this.starting = true\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: this.options.audio ?? true,\n })\n // cancel()/teardown ran while we were awaiting the mic: release the\n // freshly acquired stream and bail rather than starting a recording the\n // caller can no longer stop (a leaked live microphone).\n if (this.pendingCancel) {\n stream.getTracks().forEach((t) => t.stop())\n return\n }\n this.stream = stream\n const wanted = this.options.mimeType\n const useMimeType =\n wanted &&\n typeof MediaRecorder.isTypeSupported === 'function' &&\n MediaRecorder.isTypeSupported(wanted)\n ? wanted\n : undefined\n const recorder = useMimeType\n ? new MediaRecorder(stream, { mimeType: useMimeType })\n : new MediaRecorder(stream)\n this.chunks = []\n recorder.ondataavailable = (e) => {\n if (e.data.size > 0) {\n this.chunks.push(e.data)\n }\n }\n recorder.onstop = () => {\n void this.finalize()\n }\n recorder.onerror = (event) => {\n const detail =\n 'error' in event && event.error instanceof Error\n ? event.error\n : new Error('Audio recording failed')\n this.handleError(detail)\n }\n this.recorder = recorder\n this.startedAt = Date.now()\n recorder.start()\n this.setState('recording')\n } catch (err) {\n this.releaseStream()\n this.recorder = null\n const error =\n err instanceof Error ? err : new Error('Failed to start recording')\n this.setState('idle')\n this.notifyError(error)\n throw error\n } finally {\n this.starting = false\n // Reset here (not before the await) so a cancel() that arrives mid-start\n // is observed above; clearing it afterward keeps the next start() clean.\n this.pendingCancel = false\n }\n }\n\n stop(): Promise<AudioRecording> {\n if (this._state !== 'recording' || !this.recorder) {\n return Promise.reject(\n new Error('AudioRecorder.stop() called while not recording'),\n )\n }\n this.setState('stopping')\n const recorder = this.recorder\n return new Promise<AudioRecording>((resolve, reject) => {\n // Some browsers/codecs never fire onstop; this watchdog unwedges the\n // recorder instead of leaking this promise forever.\n const watchdog = setTimeout(() => {\n if (this._state !== 'stopping') {\n return\n }\n // Detach handlers so a late onstop/onerror from the stalled recorder\n // can't reach back in and fire finalize()/onError a second time.\n this.detachRecorder()\n if (this.chunks.length > 0) {\n // onstop never fired, but ondataavailable already delivered the\n // audio — finalize from the buffered chunks rather than discarding a\n // recording the user successfully captured.\n void this.finalize()\n } else {\n this.handleError(\n new Error('Recording stop timed out after 10s with no audio'),\n )\n }\n }, 10_000)\n this.stopResolve = (rec) => {\n clearTimeout(watchdog)\n resolve(rec)\n }\n this.stopReject = (err) => {\n clearTimeout(watchdog)\n reject(err)\n }\n recorder.stop()\n })\n }\n\n cancel(): void {\n if (this.starting) {\n // A start() is awaiting getUserMedia; flag it so the resolved stream is\n // released instead of beginning a recording with no handle to stop it.\n this.pendingCancel = true\n return\n }\n if (this._state === 'idle') {\n return\n }\n const recorder = this.recorder\n if (recorder) {\n // Detach handlers so finalize()/onError never run for a discarded\n // recording.\n this.detachRecorder()\n try {\n recorder.stop()\n } catch (err) {\n // Stopping an already-inactive recorder throws InvalidStateError —\n // that's expected here. Anything else is unexpected; surface it rather\n // than swallowing it silently.\n if (\n !(err instanceof DOMException && err.name === 'InvalidStateError')\n ) {\n this.notifyError(\n err instanceof Error ? err : new Error('Failed to stop recorder'),\n )\n }\n }\n }\n this.releaseStream()\n this.recorder = null\n this.chunks = []\n const reject = this.stopReject\n this.stopResolve = null\n this.stopReject = null\n this.setState('idle')\n reject?.(new Error('Recording cancelled'))\n }\n\n private async finalize(): Promise<void> {\n const mimeType = this.recorder?.mimeType || 'audio/webm'\n const durationMs = Date.now() - this.startedAt\n try {\n const blob = new Blob(this.chunks, { type: mimeType })\n const base64 = arrayBufferToBase64(await blob.arrayBuffer())\n const recording: AudioRecording = {\n blob,\n base64,\n mimeType,\n durationMs,\n part: {\n type: 'audio',\n source: { type: 'data', value: base64, mimeType },\n },\n }\n this.releaseStream()\n this.recorder = null\n this.chunks = []\n const resolve = this.stopResolve\n this.stopResolve = null\n this.stopReject = null\n this.setState('idle')\n resolve?.(recording)\n } catch (err) {\n this.handleError(\n err instanceof Error ? err : new Error('Failed to finalize recording'),\n )\n }\n }\n\n private handleError(error: Error): void {\n this.releaseStream()\n this.recorder = null\n this.chunks = []\n const reject = this.stopReject\n this.stopResolve = null\n this.stopReject = null\n this.setState('idle')\n // Settle the pending stop() promise before invoking the user callback so a\n // throwing onError can't strand the awaiter (the two error channels are\n // independent — see start()/stop() docs).\n reject?.(error)\n this.notifyError(error)\n }\n\n /** Invoke the user onError callback, isolating a throw so it can't disrupt\n * internal state teardown or strand a pending promise. */\n private notifyError(error: Error): void {\n try {\n this.options.onError?.(error)\n } catch {\n // A user onError that throws must not propagate into recorder internals.\n }\n }\n\n private releaseStream(): void {\n this.stream?.getTracks().forEach((t) => t.stop())\n this.stream = null\n }\n\n /** Detach all event handlers so a stale recorder can't mutate our state. */\n private detachRecorder(): void {\n const recorder = this.recorder\n if (!recorder) {\n return\n }\n recorder.onstop = null\n recorder.onerror = null\n recorder.ondataavailable = null\n }\n}\n"],"names":[],"mappings":";AA0DO,MAAM,cAAc;AAAA,EACR;AAAA,EACT,WAAiC;AAAA,EACjC,SAA6B;AAAA,EAC7B,SAAsB,CAAA;AAAA,EACtB,YAAY;AAAA,EACZ,SAA6B;AAAA;AAAA,EAE7B,WAAW;AAAA;AAAA;AAAA,EAGX,gBAAgB;AAAA,EACP,gCAAgB,IAAA;AAAA,EACzB,cAA4D;AAAA,EAC5D,aAA8C;AAAA,EAEtD,YAAY,UAAgC,IAAI;AAC9C,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,OAAO,cAAuB;AAC5B,WACE,OAAO,cAAc,eACrB,OAAO,UAAU,iBAAiB,eAClC,OAAO,UAAU,aAAa,iBAAiB,cAC/C,OAAO,kBAAkB;AAAA,EAE7B;AAAA,EAEA,IAAI,QAA4B;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,UAAU,IAAqD;AAC7D,SAAK,UAAU,IAAI,EAAE;AACrB,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,EAAE;AAAA,IAC1B;AAAA,EACF;AAAA,EAEQ,SAAS,OAAiC;AAChD,SAAK,SAAS;AACd,eAAW,MAAM,KAAK,WAAW;AAC/B,SAAG,KAAK;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,WAAW,UAAU,KAAK,UAAU;AAC3C;AAAA,IACF;AACA,SAAK,WAAW;AAChB,QAAI;AACF,YAAM,SAAS,MAAM,UAAU,aAAa,aAAa;AAAA,QACvD,OAAO,KAAK,QAAQ,SAAS;AAAA,MAAA,CAC9B;AAID,UAAI,KAAK,eAAe;AACtB,eAAO,YAAY,QAAQ,CAAC,MAAM,EAAE,MAAM;AAC1C;AAAA,MACF;AACA,WAAK,SAAS;AACd,YAAM,SAAS,KAAK,QAAQ;AAC5B,YAAM,cACJ,UACA,OAAO,cAAc,oBAAoB,cACzC,cAAc,gBAAgB,MAAM,IAChC,SACA;AACN,YAAM,WAAW,cACb,IAAI,cAAc,QAAQ,EAAE,UAAU,YAAA,CAAa,IACnD,IAAI,cAAc,MAAM;AAC5B,WAAK,SAAS,CAAA;AACd,eAAS,kBAAkB,CAAC,MAAM;AAChC,YAAI,EAAE,KAAK,OAAO,GAAG;AACnB,eAAK,OAAO,KAAK,EAAE,IAAI;AAAA,QACzB;AAAA,MACF;AACA,eAAS,SAAS,MAAM;AACtB,aAAK,KAAK,SAAA;AAAA,MACZ;AACA,eAAS,UAAU,CAAC,UAAU;AAC5B,cAAM,SACJ,WAAW,SAAS,MAAM,iBAAiB,QACvC,MAAM,QACN,IAAI,MAAM,wBAAwB;AACxC,aAAK,YAAY,MAAM;AAAA,MACzB;AACA,WAAK,WAAW;AAChB,WAAK,YAAY,KAAK,IAAA;AACtB,eAAS,MAAA;AACT,WAAK,SAAS,WAAW;AAAA,IAC3B,SAAS,KAAK;AACZ,WAAK,cAAA;AACL,WAAK,WAAW;AAChB,YAAM,QACJ,eAAe,QAAQ,MAAM,IAAI,MAAM,2BAA2B;AACpE,WAAK,SAAS,MAAM;AACpB,WAAK,YAAY,KAAK;AACtB,YAAM;AAAA,IACR,UAAA;AACE,WAAK,WAAW;AAGhB,WAAK,gBAAgB;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,OAAgC;AAC9B,QAAI,KAAK,WAAW,eAAe,CAAC,KAAK,UAAU;AACjD,aAAO,QAAQ;AAAA,QACb,IAAI,MAAM,iDAAiD;AAAA,MAAA;AAAA,IAE/D;AACA,SAAK,SAAS,UAAU;AACxB,UAAM,WAAW,KAAK;AACtB,WAAO,IAAI,QAAwB,CAAC,SAAS,WAAW;AAGtD,YAAM,WAAW,WAAW,MAAM;AAChC,YAAI,KAAK,WAAW,YAAY;AAC9B;AAAA,QACF;AAGA,aAAK,eAAA;AACL,YAAI,KAAK,OAAO,SAAS,GAAG;AAI1B,eAAK,KAAK,SAAA;AAAA,QACZ,OAAO;AACL,eAAK;AAAA,YACH,IAAI,MAAM,kDAAkD;AAAA,UAAA;AAAA,QAEhE;AAAA,MACF,GAAG,GAAM;AACT,WAAK,cAAc,CAAC,QAAQ;AAC1B,qBAAa,QAAQ;AACrB,gBAAQ,GAAG;AAAA,MACb;AACA,WAAK,aAAa,CAAC,QAAQ;AACzB,qBAAa,QAAQ;AACrB,eAAO,GAAG;AAAA,MACZ;AACA,eAAS,KAAA;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEA,SAAe;AACb,QAAI,KAAK,UAAU;AAGjB,WAAK,gBAAgB;AACrB;AAAA,IACF;AACA,QAAI,KAAK,WAAW,QAAQ;AAC1B;AAAA,IACF;AACA,UAAM,WAAW,KAAK;AACtB,QAAI,UAAU;AAGZ,WAAK,eAAA;AACL,UAAI;AACF,iBAAS,KAAA;AAAA,MACX,SAAS,KAAK;AAIZ,YACE,EAAE,eAAe,gBAAgB,IAAI,SAAS,sBAC9C;AACA,eAAK;AAAA,YACH,eAAe,QAAQ,MAAM,IAAI,MAAM,yBAAyB;AAAA,UAAA;AAAA,QAEpE;AAAA,MACF;AAAA,IACF;AACA,SAAK,cAAA;AACL,SAAK,WAAW;AAChB,SAAK,SAAS,CAAA;AACd,UAAM,SAAS,KAAK;AACpB,SAAK,cAAc;AACnB,SAAK,aAAa;AAClB,SAAK,SAAS,MAAM;AACpB,aAAS,IAAI,MAAM,qBAAqB,CAAC;AAAA,EAC3C;AAAA,EAEA,MAAc,WAA0B;AACtC,UAAM,WAAW,KAAK,UAAU,YAAY;AAC5C,UAAM,aAAa,KAAK,IAAA,IAAQ,KAAK;AACrC,QAAI;AACF,YAAM,OAAO,IAAI,KAAK,KAAK,QAAQ,EAAE,MAAM,UAAU;AACrD,YAAM,SAAS,oBAAoB,MAAM,KAAK,aAAa;AAC3D,YAAM,YAA4B;AAAA,QAChC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,QAAQ,EAAE,MAAM,QAAQ,OAAO,QAAQ,SAAA;AAAA,QAAS;AAAA,MAClD;AAEF,WAAK,cAAA;AACL,WAAK,WAAW;AAChB,WAAK,SAAS,CAAA;AACd,YAAM,UAAU,KAAK;AACrB,WAAK,cAAc;AACnB,WAAK,aAAa;AAClB,WAAK,SAAS,MAAM;AACpB,gBAAU,SAAS;AAAA,IACrB,SAAS,KAAK;AACZ,WAAK;AAAA,QACH,eAAe,QAAQ,MAAM,IAAI,MAAM,8BAA8B;AAAA,MAAA;AAAA,IAEzE;AAAA,EACF;AAAA,EAEQ,YAAY,OAAoB;AACtC,SAAK,cAAA;AACL,SAAK,WAAW;AAChB,SAAK,SAAS,CAAA;AACd,UAAM,SAAS,KAAK;AACpB,SAAK,cAAc;AACnB,SAAK,aAAa;AAClB,SAAK,SAAS,MAAM;AAIpB,aAAS,KAAK;AACd,SAAK,YAAY,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA,EAIQ,YAAY,OAAoB;AACtC,QAAI;AACF,WAAK,QAAQ,UAAU,KAAK;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,SAAK,QAAQ,YAAY,QAAQ,CAAC,MAAM,EAAE,MAAM;AAChD,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGQ,iBAAuB;AAC7B,UAAM,WAAW,KAAK;AACtB,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AACA,aAAS,SAAS;AAClB,aAAS,UAAU;AACnB,aAAS,kBAAkB;AAAA,EAC7B;AACF;"}
|
|
1
|
+
{"version":3,"file":"audio-recorder.js","names":[],"sources":["../../src/audio-recorder.ts"],"sourcesContent":["import { arrayBufferToBase64 } from '@tanstack/ai-utils'\nimport type { AudioPart } from '@tanstack/ai/client'\n\n/** Lifecycle state of an {@link AudioRecorder}. */\nexport type AudioRecorderState = 'idle' | 'recording' | 'stopping'\n\nexport interface AudioRecorderOptions {\n /** Constraints forwarded to `getUserMedia({ audio })`. Defaults to `true`. */\n audio?: MediaTrackConstraints | boolean\n /**\n * Preferred recorder mime type. Used only when\n * `MediaRecorder.isTypeSupported` reports it; otherwise the browser default\n * is used.\n */\n mimeType?: string\n /** Fired on `getUserMedia` rejection (permission denied) or recorder error. */\n onError?: (error: Error) => void\n}\n\n/**\n * Resolves the value `stop()` produces from a recorder transform callback.\n *\n * - If the callback returns a value — sync or async — that value's awaited type\n * is used (a returned `null` is a real value and is preserved).\n * - If the callback returns nothing (`void`/`undefined`), or is absent, falls\n * back to {@link AudioRecording}.\n *\n * @template TFn - The transform callback type (or undefined if not provided)\n */\nexport type InferAudioRecordingOutput<TFn> = TFn extends (\n recording: AudioRecording,\n) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? AudioRecording\n : Exclude<Awaited<R>, void | undefined>\n : AudioRecording\n\nexport interface AudioRecording {\n /** The raw recorded media blob. */\n blob: Blob\n /** Base64 of the recorded bytes (no `data:` prefix). */\n base64: string\n /** The recorder's native mime type, e.g. `audio/webm;codecs=opus`. */\n mimeType: string\n /** Recording length in milliseconds. */\n durationMs: number\n /**\n * Ready-to-use audio content part for `sendMessage`/generation prompts:\n * `{ type: 'audio', source: { type: 'data', value: base64, mimeType } }`.\n */\n part: AudioPart\n}\n\n/**\n * Framework-agnostic browser audio recorder. Wraps `getUserMedia` +\n * `MediaRecorder`, returns the recorder's native output (no transcode), and\n * builds a plug-and-play {@link AudioRecording.part}.\n */\nexport class AudioRecorder {\n private readonly options: AudioRecorderOptions\n private recorder: MediaRecorder | null = null\n private stream: MediaStream | null = null\n private chunks: Array<Blob> = []\n private startedAt = 0\n private _state: AudioRecorderState = 'idle'\n // True while start() is awaiting getUserMedia (state is still 'idle' then).\n private starting = false\n // Set by cancel()/teardown during that window so start() releases the\n // freshly acquired stream instead of beginning a leaked recording.\n private pendingCancel = false\n private readonly listeners = new Set<(state: AudioRecorderState) => void>()\n private stopResolve: ((recording: AudioRecording) => void) | null = null\n private stopReject: ((error: Error) => void) | null = null\n\n constructor(options: AudioRecorderOptions = {}) {\n this.options = options\n }\n\n /** Feature-detect the browser media APIs. SSR/Worker-safe. */\n static isSupported(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n typeof navigator.mediaDevices !== 'undefined' &&\n typeof navigator.mediaDevices.getUserMedia === 'function' &&\n typeof MediaRecorder !== 'undefined'\n )\n }\n\n get state(): AudioRecorderState {\n return this._state\n }\n\n subscribe(cb: (state: AudioRecorderState) => void): () => void {\n this.listeners.add(cb)\n return () => {\n this.listeners.delete(cb)\n }\n }\n\n private setState(state: AudioRecorderState): void {\n this._state = state\n for (const cb of this.listeners) {\n cb(state)\n }\n }\n\n async start(): Promise<void> {\n if (this._state !== 'idle' || this.starting) {\n return\n }\n this.starting = true\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: this.options.audio ?? true,\n })\n // cancel()/teardown ran while we were awaiting the mic: release the\n // freshly acquired stream and bail rather than starting a recording the\n // caller can no longer stop (a leaked live microphone).\n if (this.pendingCancel) {\n stream.getTracks().forEach((t) => t.stop())\n return\n }\n this.stream = stream\n const wanted = this.options.mimeType\n const useMimeType =\n wanted &&\n typeof MediaRecorder.isTypeSupported === 'function' &&\n MediaRecorder.isTypeSupported(wanted)\n ? wanted\n : undefined\n const recorder = useMimeType\n ? new MediaRecorder(stream, { mimeType: useMimeType })\n : new MediaRecorder(stream)\n this.chunks = []\n recorder.ondataavailable = (e) => {\n if (e.data.size > 0) {\n this.chunks.push(e.data)\n }\n }\n recorder.onstop = () => {\n void this.finalize()\n }\n recorder.onerror = (event) => {\n const detail =\n 'error' in event && event.error instanceof Error\n ? event.error\n : new Error('Audio recording failed')\n this.handleError(detail)\n }\n this.recorder = recorder\n this.startedAt = Date.now()\n recorder.start()\n this.setState('recording')\n } catch (err) {\n this.releaseStream()\n this.recorder = null\n const error =\n err instanceof Error ? err : new Error('Failed to start recording')\n this.setState('idle')\n this.notifyError(error)\n throw error\n } finally {\n this.starting = false\n // Reset here (not before the await) so a cancel() that arrives mid-start\n // is observed above; clearing it afterward keeps the next start() clean.\n this.pendingCancel = false\n }\n }\n\n stop(): Promise<AudioRecording> {\n if (this._state !== 'recording' || !this.recorder) {\n return Promise.reject(\n new Error('AudioRecorder.stop() called while not recording'),\n )\n }\n this.setState('stopping')\n const recorder = this.recorder\n return new Promise<AudioRecording>((resolve, reject) => {\n // Some browsers/codecs never fire onstop; this watchdog unwedges the\n // recorder instead of leaking this promise forever.\n const watchdog = setTimeout(() => {\n if (this._state !== 'stopping') {\n return\n }\n // Detach handlers so a late onstop/onerror from the stalled recorder\n // can't reach back in and fire finalize()/onError a second time.\n this.detachRecorder()\n if (this.chunks.length > 0) {\n // onstop never fired, but ondataavailable already delivered the\n // audio — finalize from the buffered chunks rather than discarding a\n // recording the user successfully captured.\n void this.finalize()\n } else {\n this.handleError(\n new Error('Recording stop timed out after 10s with no audio'),\n )\n }\n }, 10_000)\n this.stopResolve = (rec) => {\n clearTimeout(watchdog)\n resolve(rec)\n }\n this.stopReject = (err) => {\n clearTimeout(watchdog)\n reject(err)\n }\n recorder.stop()\n })\n }\n\n cancel(): void {\n if (this.starting) {\n // A start() is awaiting getUserMedia; flag it so the resolved stream is\n // released instead of beginning a recording with no handle to stop it.\n this.pendingCancel = true\n return\n }\n if (this._state === 'idle') {\n return\n }\n const recorder = this.recorder\n if (recorder) {\n // Detach handlers so finalize()/onError never run for a discarded\n // recording.\n this.detachRecorder()\n try {\n recorder.stop()\n } catch (err) {\n // Stopping an already-inactive recorder throws InvalidStateError —\n // that's expected here. Anything else is unexpected; surface it rather\n // than swallowing it silently.\n if (\n !(err instanceof DOMException && err.name === 'InvalidStateError')\n ) {\n this.notifyError(\n err instanceof Error ? err : new Error('Failed to stop recorder'),\n )\n }\n }\n }\n this.releaseStream()\n this.recorder = null\n this.chunks = []\n const reject = this.stopReject\n this.stopResolve = null\n this.stopReject = null\n this.setState('idle')\n reject?.(new Error('Recording cancelled'))\n }\n\n private async finalize(): Promise<void> {\n const mimeType = this.recorder?.mimeType || 'audio/webm'\n const durationMs = Date.now() - this.startedAt\n try {\n const blob = new Blob(this.chunks, { type: mimeType })\n const base64 = arrayBufferToBase64(await blob.arrayBuffer())\n const recording: AudioRecording = {\n blob,\n base64,\n mimeType,\n durationMs,\n part: {\n type: 'audio',\n source: { type: 'data', value: base64, mimeType },\n },\n }\n this.releaseStream()\n this.recorder = null\n this.chunks = []\n const resolve = this.stopResolve\n this.stopResolve = null\n this.stopReject = null\n this.setState('idle')\n resolve?.(recording)\n } catch (err) {\n this.handleError(\n err instanceof Error ? err : new Error('Failed to finalize recording'),\n )\n }\n }\n\n private handleError(error: Error): void {\n this.releaseStream()\n this.recorder = null\n this.chunks = []\n const reject = this.stopReject\n this.stopResolve = null\n this.stopReject = null\n this.setState('idle')\n // Settle the pending stop() promise before invoking the user callback so a\n // throwing onError can't strand the awaiter (the two error channels are\n // independent — see start()/stop() docs).\n reject?.(error)\n this.notifyError(error)\n }\n\n /** Invoke the user onError callback, isolating a throw so it can't disrupt\n * internal state teardown or strand a pending promise. */\n private notifyError(error: Error): void {\n try {\n this.options.onError?.(error)\n } catch {\n // A user onError that throws must not propagate into recorder internals.\n }\n }\n\n private releaseStream(): void {\n this.stream?.getTracks().forEach((t) => t.stop())\n this.stream = null\n }\n\n /** Detach all event handlers so a stale recorder can't mutate our state. */\n private detachRecorder(): void {\n const recorder = this.recorder\n if (!recorder) {\n return\n }\n recorder.onstop = null\n recorder.onerror = null\n recorder.ondataavailable = null\n }\n}\n"],"mappings":"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"}
|