@absolutejs/voice 0.0.22-beta.656 → 0.0.22-beta.658
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 +167 -67
- package/dist/client/htmxBootstrap.js +167 -67
- package/dist/client/index.js +167 -67
- package/dist/core/types.d.ts +4 -0
- package/dist/embed/index.js +167 -67
- package/dist/embed/voice-widget.js +8 -8
- package/dist/index.js +52 -31
- package/dist/react/index.js +167 -67
- package/dist/svelte/index.js +167 -67
- package/dist/testing/index.js +168 -68
- package/dist/vue/index.js +167 -67
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39835,6 +39835,7 @@ var createVoiceMonitorRunnerRoutes = (options) => {
|
|
|
39835
39835
|
};
|
|
39836
39836
|
|
|
39837
39837
|
// src/core/plugin.ts
|
|
39838
|
+
var socketIdentity = (ws) => ws.raw && typeof ws.raw === "object" ? ws.raw : ws;
|
|
39838
39839
|
var resolveQueryScenario = (query) => {
|
|
39839
39840
|
if (typeof query?.scenarioId === "string" && query.scenarioId.trim()) {
|
|
39840
39841
|
return query.scenarioId.trim();
|
|
@@ -39946,14 +39947,15 @@ var parseClientMessage = (raw) => {
|
|
|
39946
39947
|
};
|
|
39947
39948
|
var resolveSessionId3 = (runtime, ws) => {
|
|
39948
39949
|
const query = ws.data && typeof ws.data === "object" && "query" in ws.data ? ws.data.query : undefined;
|
|
39949
|
-
const
|
|
39950
|
+
const identity = socketIdentity(ws);
|
|
39951
|
+
const existing = runtime.socketSessions.get(identity);
|
|
39950
39952
|
const providedSessionId = typeof query?.sessionId === "string" && query.sessionId.trim() ? query.sessionId.trim() : existing?.sessionId ?? createId();
|
|
39951
39953
|
const scenarioId = resolveQueryScenario(query) ?? existing?.scenarioId ?? null;
|
|
39952
39954
|
const resolved = {
|
|
39953
39955
|
scenarioId,
|
|
39954
39956
|
sessionId: providedSessionId
|
|
39955
39957
|
};
|
|
39956
|
-
runtime.socketSessions.set(
|
|
39958
|
+
runtime.socketSessions.set(identity, resolved);
|
|
39957
39959
|
return resolved;
|
|
39958
39960
|
};
|
|
39959
39961
|
var resolveSocketQuery = (ws) => ws.data && typeof ws.data === "object" && ("query" in ws.data) ? ws.data.query ?? {} : {};
|
|
@@ -40158,30 +40160,48 @@ var voice = (config) => {
|
|
|
40158
40160
|
socketSessions: new WeakMap
|
|
40159
40161
|
};
|
|
40160
40162
|
const authorizedSockets = new WeakSet;
|
|
40161
|
-
const
|
|
40162
|
-
|
|
40163
|
-
|
|
40163
|
+
const pendingSocketAuthorizations = new WeakMap;
|
|
40164
|
+
const authorizeSocket = (ws, sessionId, scenarioId) => {
|
|
40165
|
+
const identity = socketIdentity(ws);
|
|
40166
|
+
const existing = pendingSocketAuthorizations.get(identity);
|
|
40167
|
+
if (existing)
|
|
40168
|
+
return existing;
|
|
40169
|
+
const authorization = (async () => {
|
|
40170
|
+
if (!config.authorizeConnection) {
|
|
40171
|
+
authorizedSockets.add(identity);
|
|
40172
|
+
return true;
|
|
40173
|
+
}
|
|
40174
|
+
let authorized = false;
|
|
40175
|
+
try {
|
|
40176
|
+
authorized = await config.authorizeConnection({
|
|
40177
|
+
context: ws.data,
|
|
40178
|
+
headers: resolveSocketHeaders(ws),
|
|
40179
|
+
path: config.path,
|
|
40180
|
+
query: resolveSocketQuery(ws),
|
|
40181
|
+
scenarioId,
|
|
40182
|
+
sessionId
|
|
40183
|
+
});
|
|
40184
|
+
} catch (error) {
|
|
40185
|
+
runtime.logger.warn?.(`[voice] connection authorization failed for session "${sessionId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
40186
|
+
}
|
|
40187
|
+
if (!authorized) {
|
|
40188
|
+
ws.close(4401, "unauthorized");
|
|
40189
|
+
return false;
|
|
40190
|
+
}
|
|
40191
|
+
authorizedSockets.add(identity);
|
|
40164
40192
|
return true;
|
|
40165
|
-
}
|
|
40166
|
-
|
|
40167
|
-
|
|
40168
|
-
|
|
40169
|
-
|
|
40170
|
-
|
|
40171
|
-
|
|
40172
|
-
|
|
40173
|
-
|
|
40174
|
-
|
|
40175
|
-
});
|
|
40176
|
-
} catch (error) {
|
|
40177
|
-
runtime.logger.warn?.(`[voice] connection authorization failed for session "${sessionId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
40178
|
-
}
|
|
40179
|
-
if (!authorized) {
|
|
40180
|
-
ws.close(4401, "unauthorized");
|
|
40193
|
+
})();
|
|
40194
|
+
pendingSocketAuthorizations.set(identity, authorization);
|
|
40195
|
+
return authorization;
|
|
40196
|
+
};
|
|
40197
|
+
const waitForSocketAuthorization = async (ws) => {
|
|
40198
|
+
const identity = socketIdentity(ws);
|
|
40199
|
+
if (authorizedSockets.has(identity))
|
|
40200
|
+
return true;
|
|
40201
|
+
const pending = pendingSocketAuthorizations.get(identity);
|
|
40202
|
+
if (!pending)
|
|
40181
40203
|
return false;
|
|
40182
|
-
|
|
40183
|
-
authorizedSockets.add(ws);
|
|
40184
|
-
return true;
|
|
40204
|
+
return pending;
|
|
40185
40205
|
};
|
|
40186
40206
|
const { monitor } = config;
|
|
40187
40207
|
const registerMonitorSession = (sessionId, handle) => {
|
|
@@ -40491,8 +40511,9 @@ var voice = (config) => {
|
|
|
40491
40511
|
};
|
|
40492
40512
|
return new Elysia70({ name: "absolutejs-voice" }).ws(config.path, {
|
|
40493
40513
|
close: async (ws, code, reason) => {
|
|
40494
|
-
const
|
|
40495
|
-
|
|
40514
|
+
const identity = socketIdentity(ws);
|
|
40515
|
+
const socketState = runtime.socketSessions.get(identity);
|
|
40516
|
+
if (!socketState || runtime.activeSockets.get(socketState.sessionId) !== identity) {
|
|
40496
40517
|
return;
|
|
40497
40518
|
}
|
|
40498
40519
|
runtime.activeSockets.delete(socketState.sessionId);
|
|
@@ -40509,7 +40530,7 @@ var voice = (config) => {
|
|
|
40509
40530
|
}
|
|
40510
40531
|
},
|
|
40511
40532
|
message: async (ws, raw) => {
|
|
40512
|
-
if (!
|
|
40533
|
+
if (!await waitForSocketAuthorization(ws)) {
|
|
40513
40534
|
ws.close(4401, "unauthorized");
|
|
40514
40535
|
return;
|
|
40515
40536
|
}
|
|
@@ -40595,8 +40616,8 @@ var voice = (config) => {
|
|
|
40595
40616
|
deregisterMonitorSession(sessionState.sessionId, "session-switch");
|
|
40596
40617
|
}
|
|
40597
40618
|
sessionState.sessionId = message.sessionId;
|
|
40598
|
-
runtime.activeSockets.set(message.sessionId, ws);
|
|
40599
|
-
runtime.socketSessions.set(ws, {
|
|
40619
|
+
runtime.activeSockets.set(message.sessionId, socketIdentity(ws));
|
|
40620
|
+
runtime.socketSessions.set(socketIdentity(ws), {
|
|
40600
40621
|
...sessionState,
|
|
40601
40622
|
sessionId: message.sessionId,
|
|
40602
40623
|
scenarioId: sessionState.scenarioId
|
|
@@ -40604,7 +40625,7 @@ var voice = (config) => {
|
|
|
40604
40625
|
}
|
|
40605
40626
|
if (message.type === "start" && message.scenarioId) {
|
|
40606
40627
|
sessionState.scenarioId = message.scenarioId;
|
|
40607
|
-
runtime.socketSessions.set(ws, {
|
|
40628
|
+
runtime.socketSessions.set(socketIdentity(ws), {
|
|
40608
40629
|
...sessionState,
|
|
40609
40630
|
scenarioId: message.scenarioId
|
|
40610
40631
|
});
|
|
@@ -40624,7 +40645,7 @@ var voice = (config) => {
|
|
|
40624
40645
|
return;
|
|
40625
40646
|
}
|
|
40626
40647
|
const existing = runtime.activeSessions.get(sessionState.sessionId);
|
|
40627
|
-
runtime.activeSockets.set(sessionState.sessionId, ws);
|
|
40648
|
+
runtime.activeSockets.set(sessionState.sessionId, socketIdentity(ws));
|
|
40628
40649
|
if (existing) {
|
|
40629
40650
|
await existing.disconnect({
|
|
40630
40651
|
code: 1006,
|
package/dist/react/index.js
CHANGED
|
@@ -11442,6 +11442,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
11442
11442
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
11443
11443
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
11444
11444
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
11445
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
11445
11446
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
11446
11447
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
11447
11448
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
@@ -11535,6 +11536,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11535
11536
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
11536
11537
|
let lifecycleGeneration = 0;
|
|
11537
11538
|
let disposed = false;
|
|
11539
|
+
let reconnectPreparationAbort = null;
|
|
11538
11540
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
11539
11541
|
const state = {
|
|
11540
11542
|
isConnected: false,
|
|
@@ -11622,12 +11624,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11622
11624
|
return;
|
|
11623
11625
|
}
|
|
11624
11626
|
try {
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11627
|
+
if (options.prepareReconnect) {
|
|
11628
|
+
const preparationAbort = new AbortController;
|
|
11629
|
+
reconnectPreparationAbort = preparationAbort;
|
|
11630
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
11631
|
+
try {
|
|
11632
|
+
await Promise.race([
|
|
11633
|
+
options.prepareReconnect({
|
|
11634
|
+
attempt: state.reconnectAttempts,
|
|
11635
|
+
path,
|
|
11636
|
+
scenarioId: state.scenarioId,
|
|
11637
|
+
sessionId: state.sessionId,
|
|
11638
|
+
signal: preparationAbort.signal
|
|
11639
|
+
}),
|
|
11640
|
+
new Promise((_, reject) => {
|
|
11641
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
11642
|
+
})
|
|
11643
|
+
]);
|
|
11644
|
+
} finally {
|
|
11645
|
+
clearTimeout(timeout);
|
|
11646
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
11647
|
+
reconnectPreparationAbort = null;
|
|
11648
|
+
}
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11631
11651
|
} catch (error) {
|
|
11632
11652
|
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
11633
11653
|
return;
|
|
@@ -11751,6 +11771,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11751
11771
|
state.ws = ws;
|
|
11752
11772
|
};
|
|
11753
11773
|
const sendSerialized = (value) => {
|
|
11774
|
+
if (disposed)
|
|
11775
|
+
return;
|
|
11754
11776
|
if (state.ws?.readyState === WS_OPEN) {
|
|
11755
11777
|
state.ws.send(value);
|
|
11756
11778
|
return;
|
|
@@ -11805,6 +11827,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11805
11827
|
};
|
|
11806
11828
|
const close = (reason = "client-close") => {
|
|
11807
11829
|
disposed = true;
|
|
11830
|
+
reconnectPreparationAbort?.abort();
|
|
11831
|
+
reconnectPreparationAbort = null;
|
|
11808
11832
|
lifecycleGeneration += 1;
|
|
11809
11833
|
clearTimers();
|
|
11810
11834
|
rejectPendingCallControls("Voice connection closed");
|
|
@@ -11822,6 +11846,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11822
11846
|
};
|
|
11823
11847
|
const disconnect = () => {
|
|
11824
11848
|
disposed = true;
|
|
11849
|
+
reconnectPreparationAbort?.abort();
|
|
11850
|
+
reconnectPreparationAbort = null;
|
|
11825
11851
|
lifecycleGeneration += 1;
|
|
11826
11852
|
clearTimers();
|
|
11827
11853
|
rejectPendingCallControls("Voice connection disconnected");
|
|
@@ -12073,7 +12099,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
12073
12099
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
12074
12100
|
}) : null;
|
|
12075
12101
|
const subscribers = new Set;
|
|
12102
|
+
let disposed = false;
|
|
12076
12103
|
const start = (input) => Promise.resolve().then(() => {
|
|
12104
|
+
if (disposed)
|
|
12105
|
+
return;
|
|
12077
12106
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
12078
12107
|
return;
|
|
12079
12108
|
}
|
|
@@ -12132,6 +12161,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
12132
12161
|
return connection.callControl(message);
|
|
12133
12162
|
},
|
|
12134
12163
|
close(reason) {
|
|
12164
|
+
disposed = true;
|
|
12135
12165
|
unsubscribeConnection();
|
|
12136
12166
|
browserMediaReporter?.close();
|
|
12137
12167
|
connection.close(reason);
|
|
@@ -12139,6 +12169,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
12139
12169
|
notify();
|
|
12140
12170
|
},
|
|
12141
12171
|
disconnect() {
|
|
12172
|
+
disposed = true;
|
|
12142
12173
|
unsubscribeConnection();
|
|
12143
12174
|
browserMediaReporter?.close();
|
|
12144
12175
|
connection.disconnect();
|
|
@@ -12336,48 +12367,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
12336
12367
|
let sourceNode = null;
|
|
12337
12368
|
let processorNode = null;
|
|
12338
12369
|
let mediaStream = null;
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12370
|
+
let lifecycleGeneration = 0;
|
|
12371
|
+
let startPromise = null;
|
|
12372
|
+
const release = (resources) => {
|
|
12373
|
+
resources.processorNode?.disconnect();
|
|
12374
|
+
resources.sourceNode?.disconnect();
|
|
12375
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
12376
|
+
resources.audioContext?.close();
|
|
12377
|
+
};
|
|
12378
|
+
const start = () => {
|
|
12379
|
+
if (mediaStream)
|
|
12380
|
+
return Promise.resolve();
|
|
12381
|
+
if (startPromise)
|
|
12382
|
+
return startPromise;
|
|
12383
|
+
const generation = ++lifecycleGeneration;
|
|
12384
|
+
const operation = async () => {
|
|
12385
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
12386
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
12387
|
+
}
|
|
12388
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
12389
|
+
if (!AudioContextCtor) {
|
|
12390
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
12391
|
+
}
|
|
12392
|
+
let pendingAudioContext = null;
|
|
12393
|
+
let pendingMediaStream = null;
|
|
12394
|
+
let pendingProcessorNode = null;
|
|
12395
|
+
let pendingSourceNode = null;
|
|
12396
|
+
try {
|
|
12397
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
12398
|
+
audio: {
|
|
12399
|
+
autoGainControl: true,
|
|
12400
|
+
channelCount: options.channelCount ?? 1,
|
|
12401
|
+
echoCancellation: true,
|
|
12402
|
+
noiseSuppression: true
|
|
12403
|
+
}
|
|
12404
|
+
});
|
|
12405
|
+
if (generation !== lifecycleGeneration) {
|
|
12406
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
12354
12407
|
}
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12408
|
+
pendingAudioContext = new AudioContextCtor;
|
|
12409
|
+
if (pendingAudioContext.state === "suspended") {
|
|
12410
|
+
await pendingAudioContext.resume();
|
|
12411
|
+
}
|
|
12412
|
+
if (generation !== lifecycleGeneration) {
|
|
12413
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
12414
|
+
}
|
|
12415
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
12416
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
12417
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
12418
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
12419
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
12420
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
12421
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
12422
|
+
options.onAudio(pcm);
|
|
12423
|
+
};
|
|
12424
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
12425
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
12426
|
+
if (generation !== lifecycleGeneration) {
|
|
12427
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
12428
|
+
}
|
|
12429
|
+
audioContext = pendingAudioContext;
|
|
12430
|
+
mediaStream = pendingMediaStream;
|
|
12431
|
+
processorNode = pendingProcessorNode;
|
|
12432
|
+
sourceNode = pendingSourceNode;
|
|
12433
|
+
} catch (error) {
|
|
12434
|
+
release({
|
|
12435
|
+
audioContext: pendingAudioContext,
|
|
12436
|
+
mediaStream: pendingMediaStream,
|
|
12437
|
+
processorNode: pendingProcessorNode,
|
|
12438
|
+
sourceNode: pendingSourceNode
|
|
12439
|
+
});
|
|
12440
|
+
throw error;
|
|
12359
12441
|
}
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
options.onAudio(pcm);
|
|
12368
|
-
};
|
|
12369
|
-
sourceNode.connect(processorNode);
|
|
12370
|
-
processorNode.connect(audioContext.destination);
|
|
12371
|
-
} catch (error) {
|
|
12372
|
-
stop();
|
|
12373
|
-
throw error;
|
|
12374
|
-
}
|
|
12442
|
+
};
|
|
12443
|
+
const pending = operation().finally(() => {
|
|
12444
|
+
if (startPromise === pending)
|
|
12445
|
+
startPromise = null;
|
|
12446
|
+
});
|
|
12447
|
+
startPromise = pending;
|
|
12448
|
+
return pending;
|
|
12375
12449
|
};
|
|
12376
12450
|
const stop = () => {
|
|
12377
|
-
|
|
12378
|
-
sourceNode
|
|
12379
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
12380
|
-
audioContext?.close();
|
|
12451
|
+
lifecycleGeneration += 1;
|
|
12452
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
12381
12453
|
options.onLevel?.(0);
|
|
12382
12454
|
audioContext = null;
|
|
12383
12455
|
mediaStream = null;
|
|
@@ -12733,6 +12805,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
12733
12805
|
...options.connection
|
|
12734
12806
|
});
|
|
12735
12807
|
let capture = null;
|
|
12808
|
+
let captureGeneration = 0;
|
|
12809
|
+
let disposed = false;
|
|
12810
|
+
let startRecordingPromise = null;
|
|
12736
12811
|
let state = createInitialState2(stream);
|
|
12737
12812
|
const subscribers = new Set;
|
|
12738
12813
|
const notify = () => {
|
|
@@ -12789,6 +12864,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
12789
12864
|
return capture;
|
|
12790
12865
|
};
|
|
12791
12866
|
const stopRecording = () => {
|
|
12867
|
+
captureGeneration += 1;
|
|
12792
12868
|
capture?.stop();
|
|
12793
12869
|
capture = null;
|
|
12794
12870
|
state = {
|
|
@@ -12798,39 +12874,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
12798
12874
|
notify();
|
|
12799
12875
|
};
|
|
12800
12876
|
const startRecording = async () => {
|
|
12877
|
+
if (disposed) {
|
|
12878
|
+
throw new Error("Voice controller is closed.");
|
|
12879
|
+
}
|
|
12801
12880
|
if (state.isRecording) {
|
|
12802
12881
|
return;
|
|
12803
12882
|
}
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12883
|
+
if (startRecordingPromise)
|
|
12884
|
+
return startRecordingPromise;
|
|
12885
|
+
const generation = ++captureGeneration;
|
|
12886
|
+
const activeCapture = ensureCapture();
|
|
12887
|
+
const operation = (async () => {
|
|
12888
|
+
try {
|
|
12889
|
+
state = {
|
|
12890
|
+
...state,
|
|
12891
|
+
recordingError: null
|
|
12892
|
+
};
|
|
12893
|
+
notify();
|
|
12894
|
+
await activeCapture.start();
|
|
12895
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
12896
|
+
activeCapture.stop();
|
|
12897
|
+
return;
|
|
12898
|
+
}
|
|
12899
|
+
state = {
|
|
12900
|
+
...state,
|
|
12901
|
+
isRecording: true
|
|
12902
|
+
};
|
|
12903
|
+
notify();
|
|
12904
|
+
} catch (error) {
|
|
12905
|
+
activeCapture.stop();
|
|
12906
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
12907
|
+
throw error;
|
|
12908
|
+
}
|
|
12909
|
+
capture = null;
|
|
12910
|
+
state = {
|
|
12911
|
+
...state,
|
|
12912
|
+
isRecording: false,
|
|
12913
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
12914
|
+
};
|
|
12915
|
+
notify();
|
|
12916
|
+
throw error;
|
|
12917
|
+
}
|
|
12918
|
+
})();
|
|
12919
|
+
const pending = operation.finally(() => {
|
|
12920
|
+
if (startRecordingPromise === pending)
|
|
12921
|
+
startRecordingPromise = null;
|
|
12922
|
+
});
|
|
12923
|
+
startRecordingPromise = pending;
|
|
12924
|
+
return startRecordingPromise;
|
|
12827
12925
|
};
|
|
12828
12926
|
const close = (reason) => {
|
|
12927
|
+
disposed = true;
|
|
12829
12928
|
unsubscribeStream();
|
|
12830
12929
|
stopRecording();
|
|
12831
12930
|
stream.close(reason);
|
|
12832
12931
|
};
|
|
12833
12932
|
const disconnect = () => {
|
|
12933
|
+
disposed = true;
|
|
12834
12934
|
unsubscribeStream();
|
|
12835
12935
|
stopRecording();
|
|
12836
12936
|
stream.disconnect();
|