ai 7.0.101 → 7.0.102
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/CHANGELOG.md +30 -0
- package/dist/index.d.ts +82 -20
- package/dist/index.js +2010 -330
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/03-agents/07-workflow-agent.mdx +1 -1
- package/docs/03-ai-sdk-core/36-realtime.mdx +41 -18
- package/docs/04-ai-sdk-ui/21-transport.mdx +1 -1
- package/docs/07-reference/02-ai-sdk-ui/05-use-realtime.mdx +364 -48
- package/docs/07-reference/04-ai-sdk-workflow/02-workflow-chat-transport.mdx +4 -4
- package/docs/07-reference/05-ai-sdk-errors/index.mdx +131 -36
- package/package.json +12 -12
- package/src/generate-text/execute-tools-from-stream.ts +7 -0
- package/src/generate-text/stream-text.ts +3 -1
- package/src/realtime/__fixtures__/fake-live-websocket.ts +71 -0
- package/src/realtime/__fixtures__/fake-realtime.ts +36 -0
- package/src/realtime/__fixtures__/fake-webrtc.ts +133 -0
- package/src/realtime/browser-realtime-audio.ts +107 -10
- package/src/realtime/browser-realtime-live-websocket.ts +247 -0
- package/src/realtime/browser-realtime-transport.ts +235 -69
- package/src/realtime/browser-realtime-webrtc.ts +582 -0
- package/src/realtime/encode-realtime-frame.ts +33 -0
- package/src/realtime/index.ts +1 -0
- package/src/realtime/realtime-attempt.ts +45 -0
- package/src/realtime/realtime-command-tracker.ts +81 -0
- package/src/realtime/realtime-event-channel.ts +170 -0
- package/src/realtime/realtime-event-reducer.ts +3 -0
- package/src/realtime/realtime-session-state.ts +65 -0
- package/src/realtime/realtime-session.ts +768 -218
- package/src/realtime/realtime-types.ts +1 -1
- package/src/realtime/validate-realtime-setup.ts +37 -0
|
@@ -10,6 +10,8 @@ export type BrowserRealtimeAudioOptions = {
|
|
|
10
10
|
onAudio: (base64Audio: string) => void;
|
|
11
11
|
onPlayingChange: (isPlaying: boolean) => void;
|
|
12
12
|
onCapturingChange: (isCapturing: boolean) => void;
|
|
13
|
+
maxPlaybackSeconds?: number;
|
|
14
|
+
onError?: (error: Error) => void;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export class BrowserRealtimeAudio {
|
|
@@ -23,6 +25,9 @@ export class BrowserRealtimeAudio {
|
|
|
23
25
|
private captureProcessor: ScriptProcessorNode | null = null;
|
|
24
26
|
private captureSource: MediaStreamAudioSourceNode | null = null;
|
|
25
27
|
private captureStream: MediaStream | null = null;
|
|
28
|
+
private ownsCaptureStream = true;
|
|
29
|
+
private captureCleanup: Array<() => void> = [];
|
|
30
|
+
private captureGeneration = 0;
|
|
26
31
|
|
|
27
32
|
private playbackContext: AudioContext | null = null;
|
|
28
33
|
private playbackQueue: Float32Array[] = [];
|
|
@@ -30,12 +35,17 @@ export class BrowserRealtimeAudio {
|
|
|
30
35
|
private playbackStartTime = 0;
|
|
31
36
|
private activeSources = new Set<AudioBufferSourceNode>();
|
|
32
37
|
private isPlaying = false;
|
|
38
|
+
private playbackPaused = false;
|
|
39
|
+
private readonly maxPlaybackSeconds: number;
|
|
40
|
+
private readonly onError: ((error: Error) => void) | undefined;
|
|
33
41
|
|
|
34
42
|
constructor(options: BrowserRealtimeAudioOptions) {
|
|
35
43
|
this.captureSampleRate = options.captureSampleRate;
|
|
36
44
|
this.playbackSampleRate = options.playbackSampleRate;
|
|
37
45
|
this.onAudio = options.onAudio;
|
|
38
46
|
this.onPlayingChange = options.onPlayingChange;
|
|
47
|
+
this.maxPlaybackSeconds = options.maxPlaybackSeconds ?? Infinity;
|
|
48
|
+
this.onError = options.onError;
|
|
39
49
|
this.onCapturingChange = options.onCapturingChange;
|
|
40
50
|
}
|
|
41
51
|
|
|
@@ -44,40 +54,101 @@ export class BrowserRealtimeAudio {
|
|
|
44
54
|
this.playbackContext = new AudioContext({
|
|
45
55
|
sampleRate: this.playbackSampleRate,
|
|
46
56
|
});
|
|
57
|
+
const context = this.playbackContext;
|
|
58
|
+
context.onstatechange = () => {
|
|
59
|
+
if (this.playbackContext === context)
|
|
60
|
+
this.setPlaying(
|
|
61
|
+
context.state === 'running' && this.activeSources.size > 0,
|
|
62
|
+
);
|
|
63
|
+
};
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
async resumePlayback(): Promise<void> {
|
|
68
|
+
this.ensurePlaybackContext();
|
|
69
|
+
const context = this.playbackContext;
|
|
70
|
+
if (this.playbackPaused) this.stopPlayback();
|
|
71
|
+
if (this.playbackContext !== context) return;
|
|
72
|
+
await context?.resume();
|
|
73
|
+
if (this.playbackContext !== context) return;
|
|
74
|
+
this.playbackPaused = false;
|
|
75
|
+
this.setPlaying(
|
|
76
|
+
this.playbackContext?.state === 'running' && this.activeSources.size > 0,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
startCapture(stream: MediaStream, options?: { ownsStream?: boolean }): void {
|
|
81
|
+
const generation = this.captureGeneration + 1;
|
|
82
|
+
this.stopCapture();
|
|
83
|
+
if (generation !== this.captureGeneration) {
|
|
84
|
+
if (options?.ownsStream ?? true)
|
|
85
|
+
stream.getTracks().forEach(track => track.stop());
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.captureStream = stream;
|
|
89
|
+
this.ownsCaptureStream = options?.ownsStream ?? true;
|
|
51
90
|
const ctx = new AudioContext({ sampleRate: this.captureSampleRate });
|
|
52
91
|
this.captureContext = ctx;
|
|
53
|
-
|
|
92
|
+
void ctx.resume().catch(error => {
|
|
93
|
+
if (this.captureContext === ctx) this.onError?.(error);
|
|
94
|
+
});
|
|
54
95
|
|
|
55
96
|
const source = ctx.createMediaStreamSource(stream);
|
|
56
97
|
this.captureSource = source;
|
|
57
98
|
|
|
58
|
-
const processor = ctx.createScriptProcessor(
|
|
99
|
+
const processor = ctx.createScriptProcessor(1024, 1, 1);
|
|
59
100
|
this.captureProcessor = processor;
|
|
60
101
|
|
|
61
102
|
processor.onaudioprocess = event => {
|
|
103
|
+
if (this.captureContext !== ctx) return;
|
|
62
104
|
const inputData = event.inputBuffer.getChannelData(0);
|
|
63
105
|
const samples = resampleAudio(
|
|
64
106
|
new Float32Array(inputData),
|
|
65
107
|
ctx.sampleRate,
|
|
66
108
|
this.captureSampleRate,
|
|
67
109
|
);
|
|
68
|
-
|
|
110
|
+
try {
|
|
111
|
+
this.onAudio(encodeRealtimeAudio(samples));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.onError?.(
|
|
114
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
69
117
|
};
|
|
70
118
|
|
|
71
119
|
source.connect(processor);
|
|
72
120
|
processor.connect(ctx.destination);
|
|
73
|
-
|
|
121
|
+
const updateCapturing = () =>
|
|
122
|
+
this.onCapturingChange(
|
|
123
|
+
stream
|
|
124
|
+
.getAudioTracks()
|
|
125
|
+
.some(
|
|
126
|
+
track =>
|
|
127
|
+
track.enabled && !track.muted && track.readyState === 'live',
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
for (const track of stream.getAudioTracks()) {
|
|
131
|
+
for (const event of ['ended', 'mute', 'unmute']) {
|
|
132
|
+
track.addEventListener(event, updateCapturing);
|
|
133
|
+
this.captureCleanup.push(() =>
|
|
134
|
+
track.removeEventListener(event, updateCapturing),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
updateCapturing();
|
|
74
139
|
}
|
|
75
140
|
|
|
76
141
|
stopCapture(): void {
|
|
142
|
+
this.captureGeneration++;
|
|
143
|
+
if (this.captureProcessor != null)
|
|
144
|
+
this.captureProcessor.onaudioprocess = null;
|
|
145
|
+
for (const cleanup of this.captureCleanup) cleanup();
|
|
146
|
+
this.captureCleanup = [];
|
|
77
147
|
this.captureProcessor?.disconnect();
|
|
78
148
|
this.captureSource?.disconnect();
|
|
79
|
-
void this.captureContext?.close();
|
|
80
|
-
this.
|
|
149
|
+
void this.captureContext?.close().catch(() => {});
|
|
150
|
+
if (this.ownsCaptureStream)
|
|
151
|
+
this.captureStream?.getTracks().forEach(track => track.stop());
|
|
81
152
|
|
|
82
153
|
this.captureProcessor = null;
|
|
83
154
|
this.captureSource = null;
|
|
@@ -87,8 +158,27 @@ export class BrowserRealtimeAudio {
|
|
|
87
158
|
}
|
|
88
159
|
|
|
89
160
|
playAudio(base64Audio: string): void {
|
|
161
|
+
if (this.playbackPaused) return;
|
|
90
162
|
this.ensurePlaybackContext();
|
|
91
|
-
|
|
163
|
+
const samples = decodeRealtimeAudio(base64Audio);
|
|
164
|
+
if (
|
|
165
|
+
Math.max(
|
|
166
|
+
0,
|
|
167
|
+
this.playbackTime - (this.playbackContext?.currentTime ?? 0),
|
|
168
|
+
) +
|
|
169
|
+
samples.length / this.playbackSampleRate >
|
|
170
|
+
this.maxPlaybackSeconds
|
|
171
|
+
) {
|
|
172
|
+
this.playbackPaused = true;
|
|
173
|
+
this.stopPlayback();
|
|
174
|
+
this.onError?.(
|
|
175
|
+
new Error(
|
|
176
|
+
'Realtime audio playback buffer is full; playback paused, call resumePlayback() to resume at the live edge',
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.playbackQueue.push(samples);
|
|
92
182
|
this.schedulePlayback();
|
|
93
183
|
}
|
|
94
184
|
|
|
@@ -97,7 +187,9 @@ export class BrowserRealtimeAudio {
|
|
|
97
187
|
|
|
98
188
|
for (const source of this.activeSources) {
|
|
99
189
|
try {
|
|
190
|
+
source.onended = null;
|
|
100
191
|
source.stop();
|
|
192
|
+
source.disconnect();
|
|
101
193
|
} catch {
|
|
102
194
|
// Source may already be stopped by the browser.
|
|
103
195
|
}
|
|
@@ -119,8 +211,11 @@ export class BrowserRealtimeAudio {
|
|
|
119
211
|
dispose(): void {
|
|
120
212
|
this.stopCapture();
|
|
121
213
|
this.stopPlayback();
|
|
122
|
-
|
|
214
|
+
if (this.playbackContext != null) this.playbackContext.onstatechange = null;
|
|
215
|
+
void this.playbackContext?.close().catch(() => {});
|
|
123
216
|
this.playbackContext = null;
|
|
217
|
+
this.playbackTime = 0;
|
|
218
|
+
this.playbackPaused = false;
|
|
124
219
|
}
|
|
125
220
|
|
|
126
221
|
private setPlaying(isPlaying: boolean): void {
|
|
@@ -155,9 +250,11 @@ export class BrowserRealtimeAudio {
|
|
|
155
250
|
this.playbackTime = startTime + buffer.duration;
|
|
156
251
|
|
|
157
252
|
this.activeSources.add(source);
|
|
158
|
-
this.setPlaying(
|
|
253
|
+
this.setPlaying(ctx.state === 'running');
|
|
254
|
+
if (this.playbackContext !== ctx) return;
|
|
159
255
|
|
|
160
256
|
source.onended = () => {
|
|
257
|
+
source.disconnect();
|
|
161
258
|
this.activeSources.delete(source);
|
|
162
259
|
if (this.playbackQueue.length === 0 && this.activeSources.size === 0) {
|
|
163
260
|
this.setPlaying(false);
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { UnsupportedFunctionalityError } from '@ai-sdk/provider';
|
|
2
|
+
import type {
|
|
3
|
+
RealtimeClientEvent,
|
|
4
|
+
RealtimeModel,
|
|
5
|
+
RealtimeServerEvent,
|
|
6
|
+
RealtimeSessionConfig,
|
|
7
|
+
} from '../types/realtime-model';
|
|
8
|
+
import { BrowserRealtimeAudio } from './browser-realtime-audio';
|
|
9
|
+
import { BrowserRealtimeTransport } from './browser-realtime-transport';
|
|
10
|
+
|
|
11
|
+
/** Continuous PCM16 audio-chunk media for an application-owned WebSocket relay. */
|
|
12
|
+
export class BrowserRealtimeLiveWebSocket {
|
|
13
|
+
private readonly transport: BrowserRealtimeTransport;
|
|
14
|
+
private readonly audio: BrowserRealtimeAudio;
|
|
15
|
+
private readonly config: RealtimeSessionConfig;
|
|
16
|
+
private generation = 0;
|
|
17
|
+
private ready = false;
|
|
18
|
+
private capturingStarted = false;
|
|
19
|
+
private stream?: MediaStream;
|
|
20
|
+
private pendingAudio = 0;
|
|
21
|
+
private captureGeneration = 0;
|
|
22
|
+
private captureEnabled = true;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly options: {
|
|
26
|
+
model: RealtimeModel;
|
|
27
|
+
sessionConfig?: Partial<RealtimeSessionConfig>;
|
|
28
|
+
sampleRate?: number;
|
|
29
|
+
maxPlaybackBufferSeconds?: number;
|
|
30
|
+
onEvent: (event: RealtimeServerEvent) => Promise<void>;
|
|
31
|
+
onError: (error: Error) => void;
|
|
32
|
+
onFatalError: (error: Error, drain?: Promise<void>) => void;
|
|
33
|
+
onClosing?: () => void;
|
|
34
|
+
onClose: (error?: Error) => void;
|
|
35
|
+
onCapturing: (value: boolean) => void;
|
|
36
|
+
onPlaying: (value: boolean) => void;
|
|
37
|
+
},
|
|
38
|
+
) {
|
|
39
|
+
const input = options.sessionConfig?.inputAudioFormat;
|
|
40
|
+
const output = options.sessionConfig?.outputAudioFormat;
|
|
41
|
+
const sampleRate =
|
|
42
|
+
input?.rate ?? output?.rate ?? options.sampleRate ?? 24000;
|
|
43
|
+
const format = {
|
|
44
|
+
type: input?.type ?? output?.type ?? 'audio/pcm',
|
|
45
|
+
rate: sampleRate,
|
|
46
|
+
};
|
|
47
|
+
this.config = {
|
|
48
|
+
...options.sessionConfig,
|
|
49
|
+
inputAudioFormat: {
|
|
50
|
+
...format,
|
|
51
|
+
...input,
|
|
52
|
+
},
|
|
53
|
+
outputAudioFormat: {
|
|
54
|
+
...format,
|
|
55
|
+
...output,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
this.transport = new BrowserRealtimeTransport({
|
|
59
|
+
model: options.model,
|
|
60
|
+
onServerEvent: options.onEvent,
|
|
61
|
+
onError: options.onError,
|
|
62
|
+
onFatalError: options.onFatalError,
|
|
63
|
+
onClosing: () => {
|
|
64
|
+
const generation = this.generation;
|
|
65
|
+
options.onClosing?.();
|
|
66
|
+
if (generation === this.generation) this.stopCapture();
|
|
67
|
+
},
|
|
68
|
+
onClose: options.onClose,
|
|
69
|
+
});
|
|
70
|
+
this.audio = new BrowserRealtimeAudio({
|
|
71
|
+
captureSampleRate: this.config.inputAudioFormat?.rate ?? sampleRate,
|
|
72
|
+
playbackSampleRate: this.config.outputAudioFormat?.rate ?? sampleRate,
|
|
73
|
+
maxPlaybackSeconds: options.maxPlaybackBufferSeconds ?? 2,
|
|
74
|
+
onAudio: audio => this.sendAudio(audio),
|
|
75
|
+
onError: options.onError,
|
|
76
|
+
onCapturingChange: options.onCapturing,
|
|
77
|
+
onPlayingChange: options.onPlaying,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
connect(options: {
|
|
82
|
+
url: string;
|
|
83
|
+
protocols?: string[];
|
|
84
|
+
stream?: MediaStream;
|
|
85
|
+
capture?: boolean;
|
|
86
|
+
}): void {
|
|
87
|
+
const generation = this.generation + 1;
|
|
88
|
+
this.dispose();
|
|
89
|
+
if (generation !== this.generation) return;
|
|
90
|
+
for (const format of [
|
|
91
|
+
this.config.inputAudioFormat,
|
|
92
|
+
this.config.outputAudioFormat,
|
|
93
|
+
]) {
|
|
94
|
+
if (format?.type !== 'audio/pcm')
|
|
95
|
+
throw new UnsupportedFunctionalityError({
|
|
96
|
+
functionality:
|
|
97
|
+
'Continuous browser WebSocket audio supports PCM16 only',
|
|
98
|
+
});
|
|
99
|
+
if (
|
|
100
|
+
format.rate == null ||
|
|
101
|
+
!Number.isFinite(format.rate) ||
|
|
102
|
+
format.rate < 8000 ||
|
|
103
|
+
format.rate > 96000
|
|
104
|
+
)
|
|
105
|
+
throw new Error('Invalid realtime PCM sample rate');
|
|
106
|
+
}
|
|
107
|
+
this.stream = options.stream;
|
|
108
|
+
this.captureEnabled = options.capture !== false;
|
|
109
|
+
// Create/resume playback while connect is still in the caller's user gesture.
|
|
110
|
+
this.audio.ensurePlaybackContext();
|
|
111
|
+
void this.audio.resumePlayback().catch(error => {
|
|
112
|
+
if (generation === this.generation) this.options.onError(error);
|
|
113
|
+
});
|
|
114
|
+
this.transport.connect({
|
|
115
|
+
mode: 'relay',
|
|
116
|
+
url: options.url,
|
|
117
|
+
protocols: options.protocols,
|
|
118
|
+
onOpen: () => {
|
|
119
|
+
return this.transport.sendEvent({
|
|
120
|
+
type: this.options.model.capabilities?.startup ?? 'session-update',
|
|
121
|
+
config: this.config,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
startCapture(): void {
|
|
128
|
+
this.ready = true;
|
|
129
|
+
if (!this.captureEnabled || this.capturingStarted) return;
|
|
130
|
+
void this.resumeCapture().catch(error => this.options.onError(error));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async resumeCapture(suppliedStream?: MediaStream): Promise<void> {
|
|
134
|
+
const generation = this.captureGeneration + 1;
|
|
135
|
+
this.stopCapture();
|
|
136
|
+
if (generation !== this.captureGeneration) return;
|
|
137
|
+
this.ready = true;
|
|
138
|
+
this.capturingStarted = true;
|
|
139
|
+
this.captureEnabled = true;
|
|
140
|
+
if (suppliedStream != null) this.stream = suppliedStream;
|
|
141
|
+
const supplied = this.stream;
|
|
142
|
+
const stream =
|
|
143
|
+
supplied ?? (await navigator.mediaDevices.getUserMedia({ audio: true }));
|
|
144
|
+
if (generation !== this.captureGeneration || !this.ready) {
|
|
145
|
+
if (supplied == null) stream.getTracks().forEach(track => track.stop());
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!stream.getAudioTracks().some(track => track.readyState === 'live')) {
|
|
149
|
+
if (supplied == null) stream.getTracks().forEach(track => track.stop());
|
|
150
|
+
throw new Error('Realtime requires a live audio track');
|
|
151
|
+
}
|
|
152
|
+
this.audio.startCapture(stream, { ownsStream: supplied == null });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private sendAudio(audio: string): void {
|
|
156
|
+
if (!this.ready || !this.transport.isOpen) return;
|
|
157
|
+
if (this.pendingAudio >= 8) {
|
|
158
|
+
this.transport.fail(new Error('Realtime audio send queue is full'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const generation = this.generation;
|
|
162
|
+
let sent: Promise<void>;
|
|
163
|
+
try {
|
|
164
|
+
sent = this.transport.sendEvent(
|
|
165
|
+
{ type: 'input-audio-append', audio },
|
|
166
|
+
undefined,
|
|
167
|
+
true,
|
|
168
|
+
);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (!this.ready || !this.transport.isOpen) return;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
this.pendingAudio++;
|
|
174
|
+
void sent.then(
|
|
175
|
+
() => {
|
|
176
|
+
if (generation === this.generation) this.pendingAudio--;
|
|
177
|
+
},
|
|
178
|
+
error => {
|
|
179
|
+
if (
|
|
180
|
+
generation === this.generation &&
|
|
181
|
+
this.ready &&
|
|
182
|
+
this.transport.isOpen
|
|
183
|
+
)
|
|
184
|
+
this.options.onFatalError(error);
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
sendEvent(
|
|
190
|
+
event: RealtimeClientEvent,
|
|
191
|
+
shouldSend?: () => boolean,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
if (event.type === 'session-update') {
|
|
194
|
+
for (const key of ['inputAudioFormat', 'outputAudioFormat'] as const) {
|
|
195
|
+
const update = event.config[key];
|
|
196
|
+
if (
|
|
197
|
+
update != null &&
|
|
198
|
+
(update.type !== this.config[key]?.type ||
|
|
199
|
+
(update.rate != null && update.rate !== this.config[key]?.rate))
|
|
200
|
+
) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
'Changing Live WebSocket audio format requires reconnecting',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return this.transport.sendEvent(event, shouldSend);
|
|
208
|
+
}
|
|
209
|
+
playAudio(audio: string): void {
|
|
210
|
+
try {
|
|
211
|
+
this.audio.playAudio(audio);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.options.onError(
|
|
214
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
stopPlayback(): void {
|
|
219
|
+
this.audio.stopPlayback();
|
|
220
|
+
}
|
|
221
|
+
resumePlayback(): Promise<void> {
|
|
222
|
+
return this.audio.resumePlayback();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
stopCapture(): void {
|
|
226
|
+
this.captureGeneration++;
|
|
227
|
+
this.ready = false;
|
|
228
|
+
this.capturingStarted = false;
|
|
229
|
+
this.captureEnabled = false;
|
|
230
|
+
this.audio.stopCapture();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
finish(): Promise<void> {
|
|
234
|
+
return this.transport.finish();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
dispose(): void {
|
|
238
|
+
this.generation++;
|
|
239
|
+
this.stopCapture();
|
|
240
|
+
this.ready = false;
|
|
241
|
+
this.capturingStarted = false;
|
|
242
|
+
this.pendingAudio = 0;
|
|
243
|
+
this.stream = undefined;
|
|
244
|
+
this.transport.disconnect();
|
|
245
|
+
this.audio.dispose();
|
|
246
|
+
}
|
|
247
|
+
}
|