@absolutejs/voice 0.0.22-beta.630 → 0.0.22-beta.632
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/client/htmxBootstrap.js +64 -1
- package/dist/client/index.js +64 -1
- package/dist/core/types.d.ts +34 -1
- package/dist/index.js +94 -1
- package/dist/testing/index.js +149 -1
- package/package.json +1 -1
|
@@ -1721,6 +1721,7 @@ var createTimeStretcher = () => {
|
|
|
1721
1721
|
};
|
|
1722
1722
|
|
|
1723
1723
|
// src/client/audioPlayer.ts
|
|
1724
|
+
var activeAudioPlayers = new Set;
|
|
1724
1725
|
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1725
1726
|
var DEFAULT_VOLUME = 1;
|
|
1726
1727
|
var DEFAULT_PLAYBACK_RATE = 1;
|
|
@@ -1729,6 +1730,7 @@ var MAX_PLAYBACK_RATE = 2;
|
|
|
1729
1730
|
var STRETCH_BYPASS_EPSILON = 0.01;
|
|
1730
1731
|
var ANALYSER_FFT_SIZE = 256;
|
|
1731
1732
|
var PCM_BYTE_MIDPOINT = 128;
|
|
1733
|
+
var GAP_EPSILON_S = 0.03;
|
|
1732
1734
|
var createInitialState3 = () => ({
|
|
1733
1735
|
activeSourceCount: 0,
|
|
1734
1736
|
error: null,
|
|
@@ -1800,6 +1802,36 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1800
1802
|
let interruptPromise = null;
|
|
1801
1803
|
let resolveInterruptPromise = null;
|
|
1802
1804
|
let interruptFallbackTimer = null;
|
|
1805
|
+
let concurrencyCounted = false;
|
|
1806
|
+
let observedPeakConcurrency = 0;
|
|
1807
|
+
let scheduledDurationMs = 0;
|
|
1808
|
+
let gapCount = 0;
|
|
1809
|
+
let totalGapMs = 0;
|
|
1810
|
+
let maxGapMs = 0;
|
|
1811
|
+
let errorCount = 0;
|
|
1812
|
+
let hasScheduledAnyChunk = false;
|
|
1813
|
+
const sampleConcurrency = () => {
|
|
1814
|
+
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
1815
|
+
observedPeakConcurrency = activeAudioPlayers.size;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
const markPlayerActive = () => {
|
|
1819
|
+
if (concurrencyCounted) {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
concurrencyCounted = true;
|
|
1823
|
+
activeAudioPlayers.add(sampleConcurrency);
|
|
1824
|
+
for (const sample of activeAudioPlayers) {
|
|
1825
|
+
sample();
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
const markPlayerInactive = () => {
|
|
1829
|
+
if (!concurrencyCounted) {
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
concurrencyCounted = false;
|
|
1833
|
+
activeAudioPlayers.delete(sampleConcurrency);
|
|
1834
|
+
};
|
|
1803
1835
|
const notify = () => {
|
|
1804
1836
|
for (const subscriber of subscribers) {
|
|
1805
1837
|
subscriber();
|
|
@@ -1913,7 +1945,17 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1913
1945
|
});
|
|
1914
1946
|
maybeResolveInterrupt();
|
|
1915
1947
|
};
|
|
1916
|
-
const
|
|
1948
|
+
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
1949
|
+
const startAt = Math.max(earliestStart, queueEndTime);
|
|
1950
|
+
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
1951
|
+
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
1952
|
+
gapCount += 1;
|
|
1953
|
+
totalGapMs += gapMs;
|
|
1954
|
+
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
1955
|
+
}
|
|
1956
|
+
hasScheduledAnyChunk = true;
|
|
1957
|
+
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
1958
|
+
sampleConcurrency();
|
|
1917
1959
|
queueEndTime = startAt + buffer.duration / rate;
|
|
1918
1960
|
sourceNodes.add(node);
|
|
1919
1961
|
setState({
|
|
@@ -1981,6 +2023,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1981
2023
|
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
1982
2024
|
});
|
|
1983
2025
|
} catch (error) {
|
|
2026
|
+
errorCount += 1;
|
|
1984
2027
|
setState({
|
|
1985
2028
|
error: error instanceof Error ? error.message : String(error)
|
|
1986
2029
|
});
|
|
@@ -2004,6 +2047,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2004
2047
|
return state.activeSourceCount;
|
|
2005
2048
|
},
|
|
2006
2049
|
close: async () => {
|
|
2050
|
+
markPlayerInactive();
|
|
2007
2051
|
unsubscribeSource();
|
|
2008
2052
|
stopQueuedPlayback({ forceClear: true });
|
|
2009
2053
|
clearInterruptTimer();
|
|
@@ -2043,6 +2087,21 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2043
2087
|
return Math.sqrt(sumSquares / analyserBuffer.length);
|
|
2044
2088
|
},
|
|
2045
2089
|
getSnapshot: () => state,
|
|
2090
|
+
getIntegritySummary: () => {
|
|
2091
|
+
const chunksReceived = source.assistantAudio.length;
|
|
2092
|
+
const chunksScheduled = state.processedChunkCount;
|
|
2093
|
+
return {
|
|
2094
|
+
chunksReceived,
|
|
2095
|
+
chunksScheduled,
|
|
2096
|
+
errorCount,
|
|
2097
|
+
gapCount,
|
|
2098
|
+
maxConcurrentPlayers: observedPeakConcurrency,
|
|
2099
|
+
maxGapMs: Math.round(maxGapMs),
|
|
2100
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
|
|
2101
|
+
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
2102
|
+
totalGapMs: Math.round(totalGapMs)
|
|
2103
|
+
};
|
|
2104
|
+
},
|
|
2046
2105
|
interrupt: async () => {
|
|
2047
2106
|
const startedAt = Date.now();
|
|
2048
2107
|
const context = await ensureAudioContext();
|
|
@@ -2096,6 +2155,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2096
2155
|
return;
|
|
2097
2156
|
}
|
|
2098
2157
|
await audioContext.suspend();
|
|
2158
|
+
markPlayerInactive();
|
|
2099
2159
|
setState({
|
|
2100
2160
|
activeSourceCount: sourceNodes.size,
|
|
2101
2161
|
isActive: false,
|
|
@@ -2126,6 +2186,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2126
2186
|
if (context.state === "suspended") {
|
|
2127
2187
|
await context.resume();
|
|
2128
2188
|
}
|
|
2189
|
+
markPlayerActive();
|
|
2129
2190
|
setState({
|
|
2130
2191
|
activeSourceCount: sourceNodes.size,
|
|
2131
2192
|
isActive: true,
|
|
@@ -2133,6 +2194,8 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2133
2194
|
});
|
|
2134
2195
|
await queueSync();
|
|
2135
2196
|
} catch (error) {
|
|
2197
|
+
errorCount += 1;
|
|
2198
|
+
markPlayerInactive();
|
|
2136
2199
|
setState({
|
|
2137
2200
|
error: error instanceof Error ? error.message : String(error),
|
|
2138
2201
|
isActive: false,
|
package/dist/client/index.js
CHANGED
|
@@ -528,6 +528,7 @@ var createTimeStretcher = () => {
|
|
|
528
528
|
};
|
|
529
529
|
|
|
530
530
|
// src/client/audioPlayer.ts
|
|
531
|
+
var activeAudioPlayers = new Set;
|
|
531
532
|
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
532
533
|
var DEFAULT_VOLUME = 1;
|
|
533
534
|
var DEFAULT_PLAYBACK_RATE = 1;
|
|
@@ -536,6 +537,7 @@ var MAX_PLAYBACK_RATE = 2;
|
|
|
536
537
|
var STRETCH_BYPASS_EPSILON = 0.01;
|
|
537
538
|
var ANALYSER_FFT_SIZE = 256;
|
|
538
539
|
var PCM_BYTE_MIDPOINT = 128;
|
|
540
|
+
var GAP_EPSILON_S = 0.03;
|
|
539
541
|
var createInitialState = () => ({
|
|
540
542
|
activeSourceCount: 0,
|
|
541
543
|
error: null,
|
|
@@ -607,6 +609,36 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
607
609
|
let interruptPromise = null;
|
|
608
610
|
let resolveInterruptPromise = null;
|
|
609
611
|
let interruptFallbackTimer = null;
|
|
612
|
+
let concurrencyCounted = false;
|
|
613
|
+
let observedPeakConcurrency = 0;
|
|
614
|
+
let scheduledDurationMs = 0;
|
|
615
|
+
let gapCount = 0;
|
|
616
|
+
let totalGapMs = 0;
|
|
617
|
+
let maxGapMs = 0;
|
|
618
|
+
let errorCount = 0;
|
|
619
|
+
let hasScheduledAnyChunk = false;
|
|
620
|
+
const sampleConcurrency = () => {
|
|
621
|
+
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
622
|
+
observedPeakConcurrency = activeAudioPlayers.size;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const markPlayerActive = () => {
|
|
626
|
+
if (concurrencyCounted) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
concurrencyCounted = true;
|
|
630
|
+
activeAudioPlayers.add(sampleConcurrency);
|
|
631
|
+
for (const sample of activeAudioPlayers) {
|
|
632
|
+
sample();
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
const markPlayerInactive = () => {
|
|
636
|
+
if (!concurrencyCounted) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
concurrencyCounted = false;
|
|
640
|
+
activeAudioPlayers.delete(sampleConcurrency);
|
|
641
|
+
};
|
|
610
642
|
const notify = () => {
|
|
611
643
|
for (const subscriber of subscribers) {
|
|
612
644
|
subscriber();
|
|
@@ -720,7 +752,17 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
720
752
|
});
|
|
721
753
|
maybeResolveInterrupt();
|
|
722
754
|
};
|
|
723
|
-
const
|
|
755
|
+
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
756
|
+
const startAt = Math.max(earliestStart, queueEndTime);
|
|
757
|
+
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
758
|
+
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
759
|
+
gapCount += 1;
|
|
760
|
+
totalGapMs += gapMs;
|
|
761
|
+
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
762
|
+
}
|
|
763
|
+
hasScheduledAnyChunk = true;
|
|
764
|
+
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
765
|
+
sampleConcurrency();
|
|
724
766
|
queueEndTime = startAt + buffer.duration / rate;
|
|
725
767
|
sourceNodes.add(node);
|
|
726
768
|
setState({
|
|
@@ -788,6 +830,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
788
830
|
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
789
831
|
});
|
|
790
832
|
} catch (error) {
|
|
833
|
+
errorCount += 1;
|
|
791
834
|
setState({
|
|
792
835
|
error: error instanceof Error ? error.message : String(error)
|
|
793
836
|
});
|
|
@@ -811,6 +854,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
811
854
|
return state.activeSourceCount;
|
|
812
855
|
},
|
|
813
856
|
close: async () => {
|
|
857
|
+
markPlayerInactive();
|
|
814
858
|
unsubscribeSource();
|
|
815
859
|
stopQueuedPlayback({ forceClear: true });
|
|
816
860
|
clearInterruptTimer();
|
|
@@ -850,6 +894,21 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
850
894
|
return Math.sqrt(sumSquares / analyserBuffer.length);
|
|
851
895
|
},
|
|
852
896
|
getSnapshot: () => state,
|
|
897
|
+
getIntegritySummary: () => {
|
|
898
|
+
const chunksReceived = source.assistantAudio.length;
|
|
899
|
+
const chunksScheduled = state.processedChunkCount;
|
|
900
|
+
return {
|
|
901
|
+
chunksReceived,
|
|
902
|
+
chunksScheduled,
|
|
903
|
+
errorCount,
|
|
904
|
+
gapCount,
|
|
905
|
+
maxConcurrentPlayers: observedPeakConcurrency,
|
|
906
|
+
maxGapMs: Math.round(maxGapMs),
|
|
907
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
|
|
908
|
+
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
909
|
+
totalGapMs: Math.round(totalGapMs)
|
|
910
|
+
};
|
|
911
|
+
},
|
|
853
912
|
interrupt: async () => {
|
|
854
913
|
const startedAt = Date.now();
|
|
855
914
|
const context = await ensureAudioContext();
|
|
@@ -903,6 +962,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
903
962
|
return;
|
|
904
963
|
}
|
|
905
964
|
await audioContext.suspend();
|
|
965
|
+
markPlayerInactive();
|
|
906
966
|
setState({
|
|
907
967
|
activeSourceCount: sourceNodes.size,
|
|
908
968
|
isActive: false,
|
|
@@ -933,6 +993,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
933
993
|
if (context.state === "suspended") {
|
|
934
994
|
await context.resume();
|
|
935
995
|
}
|
|
996
|
+
markPlayerActive();
|
|
936
997
|
setState({
|
|
937
998
|
activeSourceCount: sourceNodes.size,
|
|
938
999
|
isActive: true,
|
|
@@ -940,6 +1001,8 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
940
1001
|
});
|
|
941
1002
|
await queueSync();
|
|
942
1003
|
} catch (error) {
|
|
1004
|
+
errorCount += 1;
|
|
1005
|
+
markPlayerInactive();
|
|
943
1006
|
setState({
|
|
944
1007
|
error: error instanceof Error ? error.message : String(error),
|
|
945
1008
|
isActive: false,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -522,6 +522,17 @@ export type VoiceSessionHandle<TContext = unknown, TSession extends VoiceSession
|
|
|
522
522
|
transferMode?: "cold" | "warm";
|
|
523
523
|
}) => Promise<void>;
|
|
524
524
|
close: (reason?: string) => Promise<void>;
|
|
525
|
+
/**
|
|
526
|
+
* Caller-driven in-call pause: the session stays LIVE (socket, state,
|
|
527
|
+
* transcript) but every idle/silence watchdog is suspended and turn
|
|
528
|
+
* processing is skipped, so a paused caller is never nudged or hung up on.
|
|
529
|
+
* Past `pause.maxMs` (default 10 min) the session closes gracefully with
|
|
530
|
+
* reason "pause-timeout" — completed turns stay durably persisted, so the
|
|
531
|
+
* host's normal draft/resume path takes over.
|
|
532
|
+
*/
|
|
533
|
+
pause: () => Promise<void>;
|
|
534
|
+
/** Lift a caller-driven pause: watchdogs re-arm and turns process again. */
|
|
535
|
+
resume: () => Promise<void>;
|
|
525
536
|
snapshot: () => Promise<TSession>;
|
|
526
537
|
/**
|
|
527
538
|
* Mutate the live turn-detection config for this session — useful when a
|
|
@@ -1016,6 +1027,12 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
|
|
|
1016
1027
|
}) => string | Promise<string>);
|
|
1017
1028
|
maxReprompts?: number;
|
|
1018
1029
|
};
|
|
1030
|
+
/** Caller-driven in-call pause (VoiceSessionHandle.pause/resume). `maxMs`
|
|
1031
|
+
* bounds how long a session may sit paused before it closes gracefully
|
|
1032
|
+
* with reason "pause-timeout" (default 10 minutes). */
|
|
1033
|
+
pause?: {
|
|
1034
|
+
maxMs?: number;
|
|
1035
|
+
};
|
|
1019
1036
|
stt?: STTAdapter;
|
|
1020
1037
|
realtime?: RealtimeAdapter;
|
|
1021
1038
|
realtimeInputFormat?: AudioFormat;
|
|
@@ -1161,7 +1178,7 @@ export type VoiceClientCloseMessage = {
|
|
|
1161
1178
|
};
|
|
1162
1179
|
export type VoiceClientCallControlMessage = {
|
|
1163
1180
|
type: "call_control";
|
|
1164
|
-
action: "complete" | "escalate" | "no-answer" | "transfer" | "voicemail";
|
|
1181
|
+
action: "complete" | "escalate" | "no-answer" | "pause" | "resume" | "transfer" | "voicemail";
|
|
1165
1182
|
metadata?: Record<string, unknown>;
|
|
1166
1183
|
reason?: string;
|
|
1167
1184
|
target?: string;
|
|
@@ -1471,6 +1488,20 @@ export type VoiceAudioPlayerSource = {
|
|
|
1471
1488
|
assistantAudio: VoiceStreamState["assistantAudio"];
|
|
1472
1489
|
subscribe: (subscriber: () => void) => () => void;
|
|
1473
1490
|
};
|
|
1491
|
+
/** Post-call playback integrity, for verifying the assistant audio actually
|
|
1492
|
+
* played cleanly (no overlap, no dropped/stalled packets) — emit it as a trace
|
|
1493
|
+
* so a garbled call is provable, not guessed. `ok` is the all-clear roll-up. */
|
|
1494
|
+
export type VoiceAudioIntegrity = {
|
|
1495
|
+
ok: boolean;
|
|
1496
|
+
chunksReceived: number;
|
|
1497
|
+
chunksScheduled: number;
|
|
1498
|
+
scheduledDurationMs: number;
|
|
1499
|
+
gapCount: number;
|
|
1500
|
+
totalGapMs: number;
|
|
1501
|
+
maxGapMs: number;
|
|
1502
|
+
maxConcurrentPlayers: number;
|
|
1503
|
+
errorCount: number;
|
|
1504
|
+
};
|
|
1474
1505
|
export type VoiceAudioPlayer = {
|
|
1475
1506
|
close: () => Promise<void>;
|
|
1476
1507
|
error: string | null;
|
|
@@ -1478,6 +1509,8 @@ export type VoiceAudioPlayer = {
|
|
|
1478
1509
|
* driving a visualizer from the actual voice. 0 when idle / no analyser. */
|
|
1479
1510
|
getOutputLevel: () => number;
|
|
1480
1511
|
getSnapshot: () => VoiceAudioPlayerState;
|
|
1512
|
+
/** Post-call playback-integrity roll-up (overlap, gaps, drops). */
|
|
1513
|
+
getIntegritySummary: () => VoiceAudioIntegrity;
|
|
1481
1514
|
activeSourceCount: number;
|
|
1482
1515
|
isActive: boolean;
|
|
1483
1516
|
isPlaying: boolean;
|
package/dist/index.js
CHANGED
|
@@ -4368,6 +4368,9 @@ var createVoiceSession = (options) => {
|
|
|
4368
4368
|
if (callSilenceTimeoutMs === undefined || callSilenceFired) {
|
|
4369
4369
|
return;
|
|
4370
4370
|
}
|
|
4371
|
+
if (callerPaused) {
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4371
4374
|
clearCallSilenceWatchdog();
|
|
4372
4375
|
callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
|
|
4373
4376
|
};
|
|
@@ -4413,6 +4416,9 @@ var createVoiceSession = (options) => {
|
|
|
4413
4416
|
if (stuckCloseAfterMs === undefined || stuckCloseFired) {
|
|
4414
4417
|
return;
|
|
4415
4418
|
}
|
|
4419
|
+
if (callerPaused) {
|
|
4420
|
+
return;
|
|
4421
|
+
}
|
|
4416
4422
|
clearStuckCloseWatchdog();
|
|
4417
4423
|
stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
|
|
4418
4424
|
};
|
|
@@ -4435,6 +4441,9 @@ var createVoiceSession = (options) => {
|
|
|
4435
4441
|
idleRepromptsUsed = 0;
|
|
4436
4442
|
}
|
|
4437
4443
|
clearIdleRepromptWatchdog();
|
|
4444
|
+
if (callerPaused) {
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
4438
4447
|
if (idleRepromptsUsed >= idleRepromptMax) {
|
|
4439
4448
|
return;
|
|
4440
4449
|
}
|
|
@@ -4455,6 +4464,60 @@ var createVoiceSession = (options) => {
|
|
|
4455
4464
|
});
|
|
4456
4465
|
}, idleRepromptAfterMs);
|
|
4457
4466
|
};
|
|
4467
|
+
const PAUSE_MAX_MS_DEFAULT = 600000;
|
|
4468
|
+
const pauseMaxMs = options.pause?.maxMs && options.pause.maxMs > 0 ? options.pause.maxMs : PAUSE_MAX_MS_DEFAULT;
|
|
4469
|
+
let callerPaused = false;
|
|
4470
|
+
let pauseTimeout = null;
|
|
4471
|
+
const clearPauseTimeout = () => {
|
|
4472
|
+
if (pauseTimeout) {
|
|
4473
|
+
clearTimeout(pauseTimeout);
|
|
4474
|
+
pauseTimeout = null;
|
|
4475
|
+
}
|
|
4476
|
+
};
|
|
4477
|
+
const pauseInternal = async () => {
|
|
4478
|
+
if (callerPaused) {
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4481
|
+
const snapshot = await readSession();
|
|
4482
|
+
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
callerPaused = true;
|
|
4486
|
+
clearCallSilenceWatchdog();
|
|
4487
|
+
clearStuckCloseWatchdog();
|
|
4488
|
+
clearIdleRepromptWatchdog();
|
|
4489
|
+
clearPauseTimeout();
|
|
4490
|
+
pauseTimeout = setTimeout(() => {
|
|
4491
|
+
pauseTimeout = null;
|
|
4492
|
+
api.close("pause-timeout");
|
|
4493
|
+
}, pauseMaxMs);
|
|
4494
|
+
await appendTrace({
|
|
4495
|
+
payload: {
|
|
4496
|
+
action: "call.paused",
|
|
4497
|
+
maxMs: pauseMaxMs
|
|
4498
|
+
},
|
|
4499
|
+
session: snapshot,
|
|
4500
|
+
type: "call.lifecycle"
|
|
4501
|
+
});
|
|
4502
|
+
};
|
|
4503
|
+
const resumeInternal = async () => {
|
|
4504
|
+
if (!callerPaused) {
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
callerPaused = false;
|
|
4508
|
+
clearPauseTimeout();
|
|
4509
|
+
kickCallSilenceWatchdog();
|
|
4510
|
+
kickStuckCloseWatchdog();
|
|
4511
|
+
kickIdleRepromptWatchdog(true);
|
|
4512
|
+
const snapshot = await readSession();
|
|
4513
|
+
await appendTrace({
|
|
4514
|
+
payload: {
|
|
4515
|
+
action: "call.resumed"
|
|
4516
|
+
},
|
|
4517
|
+
session: snapshot,
|
|
4518
|
+
type: "call.lifecycle"
|
|
4519
|
+
});
|
|
4520
|
+
};
|
|
4458
4521
|
const recordingConfig = options.recording;
|
|
4459
4522
|
const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
|
|
4460
4523
|
const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
|
|
@@ -4962,6 +5025,7 @@ var createVoiceSession = (options) => {
|
|
|
4962
5025
|
clearCallSilenceWatchdog();
|
|
4963
5026
|
clearStuckCloseWatchdog();
|
|
4964
5027
|
clearIdleRepromptWatchdog();
|
|
5028
|
+
clearPauseTimeout();
|
|
4965
5029
|
clearAmdEvaluationTimer();
|
|
4966
5030
|
await closeTTSSession("failed");
|
|
4967
5031
|
await closeAdapter("failed");
|
|
@@ -5073,6 +5137,7 @@ var createVoiceSession = (options) => {
|
|
|
5073
5137
|
clearCallSilenceWatchdog();
|
|
5074
5138
|
clearStuckCloseWatchdog();
|
|
5075
5139
|
clearIdleRepromptWatchdog();
|
|
5140
|
+
clearPauseTimeout();
|
|
5076
5141
|
clearAmdEvaluationTimer();
|
|
5077
5142
|
await closeTTSSession("complete");
|
|
5078
5143
|
await closeAdapter("complete");
|
|
@@ -6033,6 +6098,19 @@ var createVoiceSession = (options) => {
|
|
|
6033
6098
|
}
|
|
6034
6099
|
kickStuckCloseWatchdog();
|
|
6035
6100
|
kickIdleRepromptWatchdog(true);
|
|
6101
|
+
if (callerPaused) {
|
|
6102
|
+
await appendTrace({
|
|
6103
|
+
payload: {
|
|
6104
|
+
action: "turn.skipped",
|
|
6105
|
+
reason: "caller-paused",
|
|
6106
|
+
status: "skipped"
|
|
6107
|
+
},
|
|
6108
|
+
session,
|
|
6109
|
+
turnId: turn.id,
|
|
6110
|
+
type: "call.lifecycle"
|
|
6111
|
+
});
|
|
6112
|
+
return;
|
|
6113
|
+
}
|
|
6036
6114
|
const liveOpsControl = await options.liveOps?.getControl(options.id);
|
|
6037
6115
|
if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
|
|
6038
6116
|
await appendTrace({
|
|
@@ -6883,6 +6961,7 @@ var createVoiceSession = (options) => {
|
|
|
6883
6961
|
clearCallSilenceWatchdog();
|
|
6884
6962
|
clearStuckCloseWatchdog();
|
|
6885
6963
|
clearIdleRepromptWatchdog();
|
|
6964
|
+
clearPauseTimeout();
|
|
6886
6965
|
clearAmdEvaluationTimer();
|
|
6887
6966
|
if (options.noiseSuppressor?.close) {
|
|
6888
6967
|
try {
|
|
@@ -6948,9 +7027,15 @@ var createVoiceSession = (options) => {
|
|
|
6948
7027
|
markVoicemail: async (input) => runSerial("api.markVoicemail", async () => {
|
|
6949
7028
|
await markVoicemailInternal(input);
|
|
6950
7029
|
}),
|
|
7030
|
+
pause: async () => runSerial("api.pause", async () => {
|
|
7031
|
+
await pauseInternal();
|
|
7032
|
+
}),
|
|
6951
7033
|
receiveAudio: async (audio) => runSerial("api.receiveAudio", async () => {
|
|
6952
7034
|
await receiveAudioInternal(audio);
|
|
6953
7035
|
}),
|
|
7036
|
+
resume: async () => runSerial("api.resume", async () => {
|
|
7037
|
+
await resumeInternal();
|
|
7038
|
+
}),
|
|
6954
7039
|
snapshot: async () => runSerial("api.snapshot", async () => readSession()),
|
|
6955
7040
|
transfer: async (input) => runSerial("api.transfer", async () => {
|
|
6956
7041
|
await transferInternal(input);
|
|
@@ -39724,7 +39809,7 @@ var isVoiceClientMessage = (value) => {
|
|
|
39724
39809
|
if (!("action" in value)) {
|
|
39725
39810
|
return false;
|
|
39726
39811
|
}
|
|
39727
|
-
if (value.action !== "complete" && value.action !== "escalate" && value.action !== "no-answer" && value.action !== "transfer" && value.action !== "voicemail") {
|
|
39812
|
+
if (value.action !== "complete" && value.action !== "escalate" && value.action !== "no-answer" && value.action !== "pause" && value.action !== "resume" && value.action !== "transfer" && value.action !== "voicemail") {
|
|
39728
39813
|
return false;
|
|
39729
39814
|
}
|
|
39730
39815
|
return (!("metadata" in value) || value.metadata === undefined || value.metadata !== null && typeof value.metadata === "object") && (!("reason" in value) || value.reason === undefined || typeof value.reason === "string") && (!("target" in value) || value.target === undefined || typeof value.target === "string");
|
|
@@ -40313,6 +40398,12 @@ var voice = (config) => {
|
|
|
40313
40398
|
if (message.action === "complete") {
|
|
40314
40399
|
await current.complete();
|
|
40315
40400
|
}
|
|
40401
|
+
if (message.action === "pause") {
|
|
40402
|
+
await current.pause();
|
|
40403
|
+
}
|
|
40404
|
+
if (message.action === "resume") {
|
|
40405
|
+
await current.resume();
|
|
40406
|
+
}
|
|
40316
40407
|
}
|
|
40317
40408
|
if (message.type === "start" && message.sessionId && message.sessionId !== sessionState.sessionId) {
|
|
40318
40409
|
const currentSession = runtime.activeSessions.get(sessionState.sessionId);
|
|
@@ -44373,7 +44464,9 @@ var createContractApi = (session) => ({
|
|
|
44373
44464
|
fail: async () => {},
|
|
44374
44465
|
markNoAnswer: async () => {},
|
|
44375
44466
|
markVoicemail: async () => {},
|
|
44467
|
+
pause: async () => {},
|
|
44376
44468
|
receiveAudio: async () => {},
|
|
44469
|
+
resume: async () => {},
|
|
44377
44470
|
setTurnDetection: async () => ({
|
|
44378
44471
|
silenceMs: 0,
|
|
44379
44472
|
speechThreshold: 0,
|
package/dist/testing/index.js
CHANGED
|
@@ -1731,6 +1731,7 @@ var createTimeStretcher = () => {
|
|
|
1731
1731
|
};
|
|
1732
1732
|
|
|
1733
1733
|
// src/client/audioPlayer.ts
|
|
1734
|
+
var activeAudioPlayers = new Set;
|
|
1734
1735
|
var DEFAULT_LOOKAHEAD_MS = 15;
|
|
1735
1736
|
var DEFAULT_VOLUME = 1;
|
|
1736
1737
|
var DEFAULT_PLAYBACK_RATE = 1;
|
|
@@ -1739,6 +1740,7 @@ var MAX_PLAYBACK_RATE = 2;
|
|
|
1739
1740
|
var STRETCH_BYPASS_EPSILON = 0.01;
|
|
1740
1741
|
var ANALYSER_FFT_SIZE = 256;
|
|
1741
1742
|
var PCM_BYTE_MIDPOINT = 128;
|
|
1743
|
+
var GAP_EPSILON_S = 0.03;
|
|
1742
1744
|
var createInitialState = () => ({
|
|
1743
1745
|
activeSourceCount: 0,
|
|
1744
1746
|
error: null,
|
|
@@ -1810,6 +1812,36 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1810
1812
|
let interruptPromise = null;
|
|
1811
1813
|
let resolveInterruptPromise = null;
|
|
1812
1814
|
let interruptFallbackTimer = null;
|
|
1815
|
+
let concurrencyCounted = false;
|
|
1816
|
+
let observedPeakConcurrency = 0;
|
|
1817
|
+
let scheduledDurationMs = 0;
|
|
1818
|
+
let gapCount = 0;
|
|
1819
|
+
let totalGapMs = 0;
|
|
1820
|
+
let maxGapMs = 0;
|
|
1821
|
+
let errorCount = 0;
|
|
1822
|
+
let hasScheduledAnyChunk = false;
|
|
1823
|
+
const sampleConcurrency = () => {
|
|
1824
|
+
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
1825
|
+
observedPeakConcurrency = activeAudioPlayers.size;
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
const markPlayerActive = () => {
|
|
1829
|
+
if (concurrencyCounted) {
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
concurrencyCounted = true;
|
|
1833
|
+
activeAudioPlayers.add(sampleConcurrency);
|
|
1834
|
+
for (const sample of activeAudioPlayers) {
|
|
1835
|
+
sample();
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
const markPlayerInactive = () => {
|
|
1839
|
+
if (!concurrencyCounted) {
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
concurrencyCounted = false;
|
|
1843
|
+
activeAudioPlayers.delete(sampleConcurrency);
|
|
1844
|
+
};
|
|
1813
1845
|
const notify = () => {
|
|
1814
1846
|
for (const subscriber of subscribers) {
|
|
1815
1847
|
subscriber();
|
|
@@ -1923,7 +1955,17 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1923
1955
|
});
|
|
1924
1956
|
maybeResolveInterrupt();
|
|
1925
1957
|
};
|
|
1926
|
-
const
|
|
1958
|
+
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
1959
|
+
const startAt = Math.max(earliestStart, queueEndTime);
|
|
1960
|
+
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
1961
|
+
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
1962
|
+
gapCount += 1;
|
|
1963
|
+
totalGapMs += gapMs;
|
|
1964
|
+
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
1965
|
+
}
|
|
1966
|
+
hasScheduledAnyChunk = true;
|
|
1967
|
+
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
1968
|
+
sampleConcurrency();
|
|
1927
1969
|
queueEndTime = startAt + buffer.duration / rate;
|
|
1928
1970
|
sourceNodes.add(node);
|
|
1929
1971
|
setState({
|
|
@@ -1991,6 +2033,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1991
2033
|
queuedChunkCount: state.queuedChunkCount + nextChunks.length
|
|
1992
2034
|
});
|
|
1993
2035
|
} catch (error) {
|
|
2036
|
+
errorCount += 1;
|
|
1994
2037
|
setState({
|
|
1995
2038
|
error: error instanceof Error ? error.message : String(error)
|
|
1996
2039
|
});
|
|
@@ -2014,6 +2057,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2014
2057
|
return state.activeSourceCount;
|
|
2015
2058
|
},
|
|
2016
2059
|
close: async () => {
|
|
2060
|
+
markPlayerInactive();
|
|
2017
2061
|
unsubscribeSource();
|
|
2018
2062
|
stopQueuedPlayback({ forceClear: true });
|
|
2019
2063
|
clearInterruptTimer();
|
|
@@ -2053,6 +2097,21 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2053
2097
|
return Math.sqrt(sumSquares / analyserBuffer.length);
|
|
2054
2098
|
},
|
|
2055
2099
|
getSnapshot: () => state,
|
|
2100
|
+
getIntegritySummary: () => {
|
|
2101
|
+
const chunksReceived = source.assistantAudio.length;
|
|
2102
|
+
const chunksScheduled = state.processedChunkCount;
|
|
2103
|
+
return {
|
|
2104
|
+
chunksReceived,
|
|
2105
|
+
chunksScheduled,
|
|
2106
|
+
errorCount,
|
|
2107
|
+
gapCount,
|
|
2108
|
+
maxConcurrentPlayers: observedPeakConcurrency,
|
|
2109
|
+
maxGapMs: Math.round(maxGapMs),
|
|
2110
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
|
|
2111
|
+
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
2112
|
+
totalGapMs: Math.round(totalGapMs)
|
|
2113
|
+
};
|
|
2114
|
+
},
|
|
2056
2115
|
interrupt: async () => {
|
|
2057
2116
|
const startedAt = Date.now();
|
|
2058
2117
|
const context = await ensureAudioContext();
|
|
@@ -2106,6 +2165,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2106
2165
|
return;
|
|
2107
2166
|
}
|
|
2108
2167
|
await audioContext.suspend();
|
|
2168
|
+
markPlayerInactive();
|
|
2109
2169
|
setState({
|
|
2110
2170
|
activeSourceCount: sourceNodes.size,
|
|
2111
2171
|
isActive: false,
|
|
@@ -2136,6 +2196,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2136
2196
|
if (context.state === "suspended") {
|
|
2137
2197
|
await context.resume();
|
|
2138
2198
|
}
|
|
2199
|
+
markPlayerActive();
|
|
2139
2200
|
setState({
|
|
2140
2201
|
activeSourceCount: sourceNodes.size,
|
|
2141
2202
|
isActive: true,
|
|
@@ -2143,6 +2204,8 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2143
2204
|
});
|
|
2144
2205
|
await queueSync();
|
|
2145
2206
|
} catch (error) {
|
|
2207
|
+
errorCount += 1;
|
|
2208
|
+
markPlayerInactive();
|
|
2146
2209
|
setState({
|
|
2147
2210
|
error: error instanceof Error ? error.message : String(error),
|
|
2148
2211
|
isActive: false,
|
|
@@ -6688,6 +6751,9 @@ var createVoiceSession = (options) => {
|
|
|
6688
6751
|
if (callSilenceTimeoutMs === undefined || callSilenceFired) {
|
|
6689
6752
|
return;
|
|
6690
6753
|
}
|
|
6754
|
+
if (callerPaused) {
|
|
6755
|
+
return;
|
|
6756
|
+
}
|
|
6691
6757
|
clearCallSilenceWatchdog();
|
|
6692
6758
|
callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
|
|
6693
6759
|
};
|
|
@@ -6733,6 +6799,9 @@ var createVoiceSession = (options) => {
|
|
|
6733
6799
|
if (stuckCloseAfterMs === undefined || stuckCloseFired) {
|
|
6734
6800
|
return;
|
|
6735
6801
|
}
|
|
6802
|
+
if (callerPaused) {
|
|
6803
|
+
return;
|
|
6804
|
+
}
|
|
6736
6805
|
clearStuckCloseWatchdog();
|
|
6737
6806
|
stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
|
|
6738
6807
|
};
|
|
@@ -6755,6 +6824,9 @@ var createVoiceSession = (options) => {
|
|
|
6755
6824
|
idleRepromptsUsed = 0;
|
|
6756
6825
|
}
|
|
6757
6826
|
clearIdleRepromptWatchdog();
|
|
6827
|
+
if (callerPaused) {
|
|
6828
|
+
return;
|
|
6829
|
+
}
|
|
6758
6830
|
if (idleRepromptsUsed >= idleRepromptMax) {
|
|
6759
6831
|
return;
|
|
6760
6832
|
}
|
|
@@ -6775,6 +6847,60 @@ var createVoiceSession = (options) => {
|
|
|
6775
6847
|
});
|
|
6776
6848
|
}, idleRepromptAfterMs);
|
|
6777
6849
|
};
|
|
6850
|
+
const PAUSE_MAX_MS_DEFAULT = 600000;
|
|
6851
|
+
const pauseMaxMs = options.pause?.maxMs && options.pause.maxMs > 0 ? options.pause.maxMs : PAUSE_MAX_MS_DEFAULT;
|
|
6852
|
+
let callerPaused = false;
|
|
6853
|
+
let pauseTimeout = null;
|
|
6854
|
+
const clearPauseTimeout = () => {
|
|
6855
|
+
if (pauseTimeout) {
|
|
6856
|
+
clearTimeout(pauseTimeout);
|
|
6857
|
+
pauseTimeout = null;
|
|
6858
|
+
}
|
|
6859
|
+
};
|
|
6860
|
+
const pauseInternal = async () => {
|
|
6861
|
+
if (callerPaused) {
|
|
6862
|
+
return;
|
|
6863
|
+
}
|
|
6864
|
+
const snapshot = await readSession();
|
|
6865
|
+
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
callerPaused = true;
|
|
6869
|
+
clearCallSilenceWatchdog();
|
|
6870
|
+
clearStuckCloseWatchdog();
|
|
6871
|
+
clearIdleRepromptWatchdog();
|
|
6872
|
+
clearPauseTimeout();
|
|
6873
|
+
pauseTimeout = setTimeout(() => {
|
|
6874
|
+
pauseTimeout = null;
|
|
6875
|
+
api.close("pause-timeout");
|
|
6876
|
+
}, pauseMaxMs);
|
|
6877
|
+
await appendTrace({
|
|
6878
|
+
payload: {
|
|
6879
|
+
action: "call.paused",
|
|
6880
|
+
maxMs: pauseMaxMs
|
|
6881
|
+
},
|
|
6882
|
+
session: snapshot,
|
|
6883
|
+
type: "call.lifecycle"
|
|
6884
|
+
});
|
|
6885
|
+
};
|
|
6886
|
+
const resumeInternal = async () => {
|
|
6887
|
+
if (!callerPaused) {
|
|
6888
|
+
return;
|
|
6889
|
+
}
|
|
6890
|
+
callerPaused = false;
|
|
6891
|
+
clearPauseTimeout();
|
|
6892
|
+
kickCallSilenceWatchdog();
|
|
6893
|
+
kickStuckCloseWatchdog();
|
|
6894
|
+
kickIdleRepromptWatchdog(true);
|
|
6895
|
+
const snapshot = await readSession();
|
|
6896
|
+
await appendTrace({
|
|
6897
|
+
payload: {
|
|
6898
|
+
action: "call.resumed"
|
|
6899
|
+
},
|
|
6900
|
+
session: snapshot,
|
|
6901
|
+
type: "call.lifecycle"
|
|
6902
|
+
});
|
|
6903
|
+
};
|
|
6778
6904
|
const recordingConfig = options.recording;
|
|
6779
6905
|
const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
|
|
6780
6906
|
const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
|
|
@@ -7282,6 +7408,7 @@ var createVoiceSession = (options) => {
|
|
|
7282
7408
|
clearCallSilenceWatchdog();
|
|
7283
7409
|
clearStuckCloseWatchdog();
|
|
7284
7410
|
clearIdleRepromptWatchdog();
|
|
7411
|
+
clearPauseTimeout();
|
|
7285
7412
|
clearAmdEvaluationTimer();
|
|
7286
7413
|
await closeTTSSession("failed");
|
|
7287
7414
|
await closeAdapter("failed");
|
|
@@ -7393,6 +7520,7 @@ var createVoiceSession = (options) => {
|
|
|
7393
7520
|
clearCallSilenceWatchdog();
|
|
7394
7521
|
clearStuckCloseWatchdog();
|
|
7395
7522
|
clearIdleRepromptWatchdog();
|
|
7523
|
+
clearPauseTimeout();
|
|
7396
7524
|
clearAmdEvaluationTimer();
|
|
7397
7525
|
await closeTTSSession("complete");
|
|
7398
7526
|
await closeAdapter("complete");
|
|
@@ -8353,6 +8481,19 @@ var createVoiceSession = (options) => {
|
|
|
8353
8481
|
}
|
|
8354
8482
|
kickStuckCloseWatchdog();
|
|
8355
8483
|
kickIdleRepromptWatchdog(true);
|
|
8484
|
+
if (callerPaused) {
|
|
8485
|
+
await appendTrace({
|
|
8486
|
+
payload: {
|
|
8487
|
+
action: "turn.skipped",
|
|
8488
|
+
reason: "caller-paused",
|
|
8489
|
+
status: "skipped"
|
|
8490
|
+
},
|
|
8491
|
+
session,
|
|
8492
|
+
turnId: turn.id,
|
|
8493
|
+
type: "call.lifecycle"
|
|
8494
|
+
});
|
|
8495
|
+
return;
|
|
8496
|
+
}
|
|
8356
8497
|
const liveOpsControl = await options.liveOps?.getControl(options.id);
|
|
8357
8498
|
if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
|
|
8358
8499
|
await appendTrace({
|
|
@@ -9203,6 +9344,7 @@ var createVoiceSession = (options) => {
|
|
|
9203
9344
|
clearCallSilenceWatchdog();
|
|
9204
9345
|
clearStuckCloseWatchdog();
|
|
9205
9346
|
clearIdleRepromptWatchdog();
|
|
9347
|
+
clearPauseTimeout();
|
|
9206
9348
|
clearAmdEvaluationTimer();
|
|
9207
9349
|
if (options.noiseSuppressor?.close) {
|
|
9208
9350
|
try {
|
|
@@ -9268,9 +9410,15 @@ var createVoiceSession = (options) => {
|
|
|
9268
9410
|
markVoicemail: async (input) => runSerial("api.markVoicemail", async () => {
|
|
9269
9411
|
await markVoicemailInternal(input);
|
|
9270
9412
|
}),
|
|
9413
|
+
pause: async () => runSerial("api.pause", async () => {
|
|
9414
|
+
await pauseInternal();
|
|
9415
|
+
}),
|
|
9271
9416
|
receiveAudio: async (audio) => runSerial("api.receiveAudio", async () => {
|
|
9272
9417
|
await receiveAudioInternal(audio);
|
|
9273
9418
|
}),
|
|
9419
|
+
resume: async () => runSerial("api.resume", async () => {
|
|
9420
|
+
await resumeInternal();
|
|
9421
|
+
}),
|
|
9274
9422
|
snapshot: async () => runSerial("api.snapshot", async () => readSession()),
|
|
9275
9423
|
transfer: async (input) => runSerial("api.transfer", async () => {
|
|
9276
9424
|
await transferInternal(input);
|