@absolutejs/voice 0.0.22-beta.655 → 0.0.22-beta.657
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/dist/angular/index.js +204 -70
- package/dist/client/connection.d.ts +3 -0
- package/dist/client/htmxBootstrap.js +204 -70
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +206 -71
- package/dist/core/types.d.ts +4 -0
- package/dist/embed/index.js +204 -70
- package/dist/embed/voice-widget.js +8 -8
- package/dist/react/index.js +204 -70
- package/dist/svelte/index.js +204 -70
- package/dist/testing/index.js +205 -71
- package/dist/vue/index.js +204 -70
- package/package.json +1 -1
package/dist/testing/index.js
CHANGED
|
@@ -2738,48 +2738,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
2738
2738
|
let sourceNode = null;
|
|
2739
2739
|
let processorNode = null;
|
|
2740
2740
|
let mediaStream = null;
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2741
|
+
let lifecycleGeneration = 0;
|
|
2742
|
+
let startPromise = null;
|
|
2743
|
+
const release = (resources) => {
|
|
2744
|
+
resources.processorNode?.disconnect();
|
|
2745
|
+
resources.sourceNode?.disconnect();
|
|
2746
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
2747
|
+
resources.audioContext?.close();
|
|
2748
|
+
};
|
|
2749
|
+
const start = () => {
|
|
2750
|
+
if (mediaStream)
|
|
2751
|
+
return Promise.resolve();
|
|
2752
|
+
if (startPromise)
|
|
2753
|
+
return startPromise;
|
|
2754
|
+
const generation = ++lifecycleGeneration;
|
|
2755
|
+
const operation = async () => {
|
|
2756
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
2757
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
2758
|
+
}
|
|
2759
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
2760
|
+
if (!AudioContextCtor) {
|
|
2761
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
2762
|
+
}
|
|
2763
|
+
let pendingAudioContext = null;
|
|
2764
|
+
let pendingMediaStream = null;
|
|
2765
|
+
let pendingProcessorNode = null;
|
|
2766
|
+
let pendingSourceNode = null;
|
|
2767
|
+
try {
|
|
2768
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
2769
|
+
audio: {
|
|
2770
|
+
autoGainControl: true,
|
|
2771
|
+
channelCount: options.channelCount ?? 1,
|
|
2772
|
+
echoCancellation: true,
|
|
2773
|
+
noiseSuppression: true
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
if (generation !== lifecycleGeneration) {
|
|
2777
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
2756
2778
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2779
|
+
pendingAudioContext = new AudioContextCtor;
|
|
2780
|
+
if (pendingAudioContext.state === "suspended") {
|
|
2781
|
+
await pendingAudioContext.resume();
|
|
2782
|
+
}
|
|
2783
|
+
if (generation !== lifecycleGeneration) {
|
|
2784
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
2785
|
+
}
|
|
2786
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
2787
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
2788
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
2789
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
2790
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
2791
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
2792
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
2793
|
+
options.onAudio(pcm);
|
|
2794
|
+
};
|
|
2795
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
2796
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
2797
|
+
if (generation !== lifecycleGeneration) {
|
|
2798
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
2799
|
+
}
|
|
2800
|
+
audioContext = pendingAudioContext;
|
|
2801
|
+
mediaStream = pendingMediaStream;
|
|
2802
|
+
processorNode = pendingProcessorNode;
|
|
2803
|
+
sourceNode = pendingSourceNode;
|
|
2804
|
+
} catch (error) {
|
|
2805
|
+
release({
|
|
2806
|
+
audioContext: pendingAudioContext,
|
|
2807
|
+
mediaStream: pendingMediaStream,
|
|
2808
|
+
processorNode: pendingProcessorNode,
|
|
2809
|
+
sourceNode: pendingSourceNode
|
|
2810
|
+
});
|
|
2811
|
+
throw error;
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
const pending = operation().finally(() => {
|
|
2815
|
+
if (startPromise === pending)
|
|
2816
|
+
startPromise = null;
|
|
2817
|
+
});
|
|
2818
|
+
startPromise = pending;
|
|
2819
|
+
return pending;
|
|
2777
2820
|
};
|
|
2778
2821
|
const stop = () => {
|
|
2779
|
-
|
|
2780
|
-
sourceNode
|
|
2781
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
2782
|
-
audioContext?.close();
|
|
2822
|
+
lifecycleGeneration += 1;
|
|
2823
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
2783
2824
|
options.onLevel?.(0);
|
|
2784
2825
|
audioContext = null;
|
|
2785
2826
|
mediaStream = null;
|
|
@@ -2991,11 +3032,19 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
2991
3032
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
2992
3033
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
2993
3034
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
3035
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
2994
3036
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
2995
3037
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
2996
3038
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
2997
3039
|
var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
|
|
2998
3040
|
var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
|
|
3041
|
+
|
|
3042
|
+
class VoiceReconnectRejectedError extends Error {
|
|
3043
|
+
constructor(message = "Voice authorization could not be renewed.") {
|
|
3044
|
+
super(message);
|
|
3045
|
+
this.name = "VoiceReconnectRejectedError";
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
2999
3048
|
var noop = () => {};
|
|
3000
3049
|
var noopAsync = () => Promise.resolve();
|
|
3001
3050
|
var noopUnsubscribe = () => noop;
|
|
@@ -3075,6 +3124,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3075
3124
|
const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
3076
3125
|
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
3077
3126
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
3127
|
+
let lifecycleGeneration = 0;
|
|
3128
|
+
let disposed = false;
|
|
3129
|
+
let reconnectPreparationAbort = null;
|
|
3078
3130
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
3079
3131
|
const state = {
|
|
3080
3132
|
isConnected: false,
|
|
@@ -3138,6 +3190,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3138
3190
|
pendingCallControls.clear();
|
|
3139
3191
|
};
|
|
3140
3192
|
const scheduleReconnect = () => {
|
|
3193
|
+
if (disposed)
|
|
3194
|
+
return;
|
|
3195
|
+
const scheduledGeneration = lifecycleGeneration;
|
|
3141
3196
|
state.reconnectAttempts += 1;
|
|
3142
3197
|
const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
|
|
3143
3198
|
const nextAttemptAt = Date.now() + delayMs;
|
|
@@ -3152,18 +3207,44 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3152
3207
|
type: "connection"
|
|
3153
3208
|
});
|
|
3154
3209
|
state.reconnectTimeout = setTimeout(async () => {
|
|
3210
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
3211
|
+
return;
|
|
3155
3212
|
if (state.reconnectAttempts > maxReconnectAttempts) {
|
|
3156
3213
|
emitTerminalFailure("Voice connection could not be restored.");
|
|
3157
3214
|
return;
|
|
3158
3215
|
}
|
|
3159
3216
|
try {
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3217
|
+
if (options.prepareReconnect) {
|
|
3218
|
+
const preparationAbort = new AbortController;
|
|
3219
|
+
reconnectPreparationAbort = preparationAbort;
|
|
3220
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
3221
|
+
try {
|
|
3222
|
+
await Promise.race([
|
|
3223
|
+
options.prepareReconnect({
|
|
3224
|
+
attempt: state.reconnectAttempts,
|
|
3225
|
+
path,
|
|
3226
|
+
scenarioId: state.scenarioId,
|
|
3227
|
+
sessionId: state.sessionId,
|
|
3228
|
+
signal: preparationAbort.signal
|
|
3229
|
+
}),
|
|
3230
|
+
new Promise((_, reject) => {
|
|
3231
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
3232
|
+
})
|
|
3233
|
+
]);
|
|
3234
|
+
} finally {
|
|
3235
|
+
clearTimeout(timeout);
|
|
3236
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
3237
|
+
reconnectPreparationAbort = null;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
3243
|
+
return;
|
|
3244
|
+
if (error instanceof VoiceReconnectRejectedError) {
|
|
3245
|
+
emitTerminalFailure(error.message);
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3167
3248
|
if (state.reconnectAttempts < maxReconnectAttempts) {
|
|
3168
3249
|
scheduleReconnect();
|
|
3169
3250
|
} else {
|
|
@@ -3171,10 +3252,14 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3171
3252
|
}
|
|
3172
3253
|
return;
|
|
3173
3254
|
}
|
|
3255
|
+
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
3256
|
+
return;
|
|
3174
3257
|
connect();
|
|
3175
3258
|
}, delayMs);
|
|
3176
3259
|
};
|
|
3177
3260
|
const connect = () => {
|
|
3261
|
+
if (disposed)
|
|
3262
|
+
return;
|
|
3178
3263
|
const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
|
|
3179
3264
|
ws.binaryType = "arraybuffer";
|
|
3180
3265
|
ws.onopen = () => {
|
|
@@ -3245,6 +3330,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3245
3330
|
state.ws = null;
|
|
3246
3331
|
state.isConnected = false;
|
|
3247
3332
|
clearTimers();
|
|
3333
|
+
rejectPendingCallControls("Voice connection interrupted");
|
|
3248
3334
|
const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && state.reconnectAttempts < maxReconnectAttempts;
|
|
3249
3335
|
const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
|
|
3250
3336
|
if (authorizationFailure) {
|
|
@@ -3275,6 +3361,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3275
3361
|
state.ws = ws;
|
|
3276
3362
|
};
|
|
3277
3363
|
const sendSerialized = (value) => {
|
|
3364
|
+
if (disposed)
|
|
3365
|
+
return;
|
|
3278
3366
|
if (state.ws?.readyState === WS_OPEN) {
|
|
3279
3367
|
state.ws.send(value);
|
|
3280
3368
|
return;
|
|
@@ -3310,6 +3398,9 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3310
3398
|
send({ type: "end_turn" });
|
|
3311
3399
|
};
|
|
3312
3400
|
const callControl = (message) => {
|
|
3401
|
+
if (state.ws?.readyState !== WS_OPEN) {
|
|
3402
|
+
return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
|
|
3403
|
+
}
|
|
3313
3404
|
const requestId = crypto.randomUUID();
|
|
3314
3405
|
const { promise, reject, resolve } = Promise.withResolvers();
|
|
3315
3406
|
const timeout = setTimeout(() => {
|
|
@@ -3317,16 +3408,21 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3317
3408
|
reject(new Error(`${message.action} acknowledgement timed out`));
|
|
3318
3409
|
}, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
|
|
3319
3410
|
pendingCallControls.set(requestId, { reject, resolve, timeout });
|
|
3320
|
-
send({
|
|
3411
|
+
state.ws.send(JSON.stringify({
|
|
3321
3412
|
...message,
|
|
3322
3413
|
requestId,
|
|
3323
3414
|
type: "call_control"
|
|
3324
|
-
});
|
|
3415
|
+
}));
|
|
3325
3416
|
return promise;
|
|
3326
3417
|
};
|
|
3327
3418
|
const close = (reason = "client-close") => {
|
|
3419
|
+
disposed = true;
|
|
3420
|
+
reconnectPreparationAbort?.abort();
|
|
3421
|
+
reconnectPreparationAbort = null;
|
|
3422
|
+
lifecycleGeneration += 1;
|
|
3328
3423
|
clearTimers();
|
|
3329
3424
|
rejectPendingCallControls("Voice connection closed");
|
|
3425
|
+
state.pendingMessages.length = 0;
|
|
3330
3426
|
if (state.ws) {
|
|
3331
3427
|
const ws = state.ws;
|
|
3332
3428
|
state.ws = null;
|
|
@@ -3339,8 +3435,13 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3339
3435
|
listeners.clear();
|
|
3340
3436
|
};
|
|
3341
3437
|
const disconnect = () => {
|
|
3438
|
+
disposed = true;
|
|
3439
|
+
reconnectPreparationAbort?.abort();
|
|
3440
|
+
reconnectPreparationAbort = null;
|
|
3441
|
+
lifecycleGeneration += 1;
|
|
3342
3442
|
clearTimers();
|
|
3343
3443
|
rejectPendingCallControls("Voice connection disconnected");
|
|
3444
|
+
state.pendingMessages.length = 0;
|
|
3344
3445
|
if (state.ws) {
|
|
3345
3446
|
const ws = state.ws;
|
|
3346
3447
|
state.ws = null;
|
|
@@ -3588,7 +3689,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
3588
3689
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
3589
3690
|
}) : null;
|
|
3590
3691
|
const subscribers = new Set;
|
|
3692
|
+
let disposed = false;
|
|
3591
3693
|
const start = (input) => Promise.resolve().then(() => {
|
|
3694
|
+
if (disposed)
|
|
3695
|
+
return;
|
|
3592
3696
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
3593
3697
|
return;
|
|
3594
3698
|
}
|
|
@@ -3647,6 +3751,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
3647
3751
|
return connection.callControl(message);
|
|
3648
3752
|
},
|
|
3649
3753
|
close(reason) {
|
|
3754
|
+
disposed = true;
|
|
3650
3755
|
unsubscribeConnection();
|
|
3651
3756
|
browserMediaReporter?.close();
|
|
3652
3757
|
connection.close(reason);
|
|
@@ -3654,6 +3759,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
3654
3759
|
notify();
|
|
3655
3760
|
},
|
|
3656
3761
|
disconnect() {
|
|
3762
|
+
disposed = true;
|
|
3657
3763
|
unsubscribeConnection();
|
|
3658
3764
|
browserMediaReporter?.close();
|
|
3659
3765
|
connection.disconnect();
|
|
@@ -4057,6 +4163,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
4057
4163
|
...options.connection
|
|
4058
4164
|
});
|
|
4059
4165
|
let capture = null;
|
|
4166
|
+
let captureGeneration = 0;
|
|
4167
|
+
let disposed = false;
|
|
4168
|
+
let startRecordingPromise = null;
|
|
4060
4169
|
let state = createInitialState3(stream);
|
|
4061
4170
|
const subscribers = new Set;
|
|
4062
4171
|
const notify = () => {
|
|
@@ -4113,6 +4222,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
4113
4222
|
return capture;
|
|
4114
4223
|
};
|
|
4115
4224
|
const stopRecording = () => {
|
|
4225
|
+
captureGeneration += 1;
|
|
4116
4226
|
capture?.stop();
|
|
4117
4227
|
capture = null;
|
|
4118
4228
|
state = {
|
|
@@ -4122,39 +4232,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
4122
4232
|
notify();
|
|
4123
4233
|
};
|
|
4124
4234
|
const startRecording = async () => {
|
|
4235
|
+
if (disposed) {
|
|
4236
|
+
throw new Error("Voice controller is closed.");
|
|
4237
|
+
}
|
|
4125
4238
|
if (state.isRecording) {
|
|
4126
4239
|
return;
|
|
4127
4240
|
}
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4241
|
+
if (startRecordingPromise)
|
|
4242
|
+
return startRecordingPromise;
|
|
4243
|
+
const generation = ++captureGeneration;
|
|
4244
|
+
const activeCapture = ensureCapture();
|
|
4245
|
+
const operation = (async () => {
|
|
4246
|
+
try {
|
|
4247
|
+
state = {
|
|
4248
|
+
...state,
|
|
4249
|
+
recordingError: null
|
|
4250
|
+
};
|
|
4251
|
+
notify();
|
|
4252
|
+
await activeCapture.start();
|
|
4253
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
4254
|
+
activeCapture.stop();
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
state = {
|
|
4258
|
+
...state,
|
|
4259
|
+
isRecording: true
|
|
4260
|
+
};
|
|
4261
|
+
notify();
|
|
4262
|
+
} catch (error) {
|
|
4263
|
+
activeCapture.stop();
|
|
4264
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
4265
|
+
throw error;
|
|
4266
|
+
}
|
|
4267
|
+
capture = null;
|
|
4268
|
+
state = {
|
|
4269
|
+
...state,
|
|
4270
|
+
isRecording: false,
|
|
4271
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
4272
|
+
};
|
|
4273
|
+
notify();
|
|
4274
|
+
throw error;
|
|
4275
|
+
}
|
|
4276
|
+
})();
|
|
4277
|
+
const pending = operation.finally(() => {
|
|
4278
|
+
if (startRecordingPromise === pending)
|
|
4279
|
+
startRecordingPromise = null;
|
|
4280
|
+
});
|
|
4281
|
+
startRecordingPromise = pending;
|
|
4282
|
+
return startRecordingPromise;
|
|
4151
4283
|
};
|
|
4152
4284
|
const close = (reason) => {
|
|
4285
|
+
disposed = true;
|
|
4153
4286
|
unsubscribeStream();
|
|
4154
4287
|
stopRecording();
|
|
4155
4288
|
stream.close(reason);
|
|
4156
4289
|
};
|
|
4157
4290
|
const disconnect = () => {
|
|
4291
|
+
disposed = true;
|
|
4158
4292
|
unsubscribeStream();
|
|
4159
4293
|
stopRecording();
|
|
4160
4294
|
stream.disconnect();
|