@absolutejs/voice 0.0.22-beta.321 → 0.0.22-beta.323
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/angular/index.js +233 -7
- package/dist/browserMediaRoutes.d.ts +2 -1
- package/dist/client/htmxBootstrap.js +69 -7
- package/dist/client/index.js +233 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +816 -444
- package/dist/react/index.js +233 -7
- package/dist/svelte/index.js +233 -7
- package/dist/telephonyMediaRoutes.d.ts +63 -0
- package/dist/testing/index.js +246 -20
- package/dist/types.d.ts +3 -1
- package/dist/vue/index.js +233 -7
- package/package.json +2 -2
package/dist/client/index.js
CHANGED
|
@@ -805,7 +805,65 @@ var stringStat = (stat, key) => {
|
|
|
805
805
|
const value = stat[key];
|
|
806
806
|
return typeof value === "string" ? value : undefined;
|
|
807
807
|
};
|
|
808
|
+
var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
|
|
808
809
|
var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
810
|
+
var DEFAULT_TELEPHONY_FORMAT = {
|
|
811
|
+
channels: 1,
|
|
812
|
+
container: "raw",
|
|
813
|
+
encoding: "mulaw",
|
|
814
|
+
sampleRateHz: 8000
|
|
815
|
+
};
|
|
816
|
+
var bytesToBase64 = (audio) => {
|
|
817
|
+
const bytes = audio instanceof ArrayBuffer ? new Uint8Array(audio) : new Uint8Array(audio.buffer, audio.byteOffset, audio.byteLength);
|
|
818
|
+
return Buffer.from(bytes).toString("base64");
|
|
819
|
+
};
|
|
820
|
+
var base64ToBytes = (value) => new Uint8Array(Buffer.from(value, "base64"));
|
|
821
|
+
var unknownRecord = (value) => value && typeof value === "object" ? value : {};
|
|
822
|
+
var firstString = (records, keys) => {
|
|
823
|
+
for (const record of records) {
|
|
824
|
+
for (const key of keys) {
|
|
825
|
+
const value = record[key];
|
|
826
|
+
if (typeof value === "string" && value.length > 0) {
|
|
827
|
+
return value;
|
|
828
|
+
}
|
|
829
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
830
|
+
return String(value);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
};
|
|
836
|
+
var firstNumber = (records, keys) => {
|
|
837
|
+
for (const record of records) {
|
|
838
|
+
for (const key of keys) {
|
|
839
|
+
const value = record[key];
|
|
840
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
841
|
+
return value;
|
|
842
|
+
}
|
|
843
|
+
if (typeof value === "string") {
|
|
844
|
+
const parsed = Number(value);
|
|
845
|
+
if (Number.isFinite(parsed)) {
|
|
846
|
+
return parsed;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
};
|
|
853
|
+
var telephonyDirection = (track) => {
|
|
854
|
+
const normalized = track?.toLowerCase();
|
|
855
|
+
if (!normalized) {
|
|
856
|
+
return "unknown";
|
|
857
|
+
}
|
|
858
|
+
if (normalized.includes("inbound") || normalized.includes("caller") || normalized.includes("in")) {
|
|
859
|
+
return "inbound";
|
|
860
|
+
}
|
|
861
|
+
if (normalized.includes("outbound") || normalized.includes("assistant") || normalized.includes("out")) {
|
|
862
|
+
return "outbound";
|
|
863
|
+
}
|
|
864
|
+
return "unknown";
|
|
865
|
+
};
|
|
866
|
+
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
809
867
|
var normalizeWebRTCStat = (stat) => {
|
|
810
868
|
const sample = {};
|
|
811
869
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -815,6 +873,113 @@ var normalizeWebRTCStat = (stat) => {
|
|
|
815
873
|
}
|
|
816
874
|
return sample;
|
|
817
875
|
};
|
|
876
|
+
var parseTelephonyMediaFrame = (input) => {
|
|
877
|
+
const envelope = input.envelope;
|
|
878
|
+
const media = unknownRecord(envelope.media);
|
|
879
|
+
const payload = firstString([media, envelope], ["payload", "audio", "data"]) ?? firstString([unknownRecord(envelope.message)], ["payload"]);
|
|
880
|
+
if (!payload) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const carrier = input.carrier ?? firstString([envelope], ["provider"]) ?? "telephony";
|
|
884
|
+
const streamId = firstString([media, envelope], ["streamSid", "stream_id", "streamId", "streamId", "callSid", "call_id"]);
|
|
885
|
+
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
886
|
+
const track = firstString([media, envelope], ["track", "direction"]);
|
|
887
|
+
const direction = telephonyDirection(track);
|
|
888
|
+
const timestamp = firstNumber([media, envelope], ["timestamp", "time", "startedAt"]);
|
|
889
|
+
return {
|
|
890
|
+
at: timestamp,
|
|
891
|
+
audio: base64ToBytes(payload),
|
|
892
|
+
format: input.format ?? DEFAULT_TELEPHONY_FORMAT,
|
|
893
|
+
id: [
|
|
894
|
+
carrier,
|
|
895
|
+
streamId ?? input.sessionId ?? "stream",
|
|
896
|
+
sequenceNumber ?? timestamp ?? Date.now()
|
|
897
|
+
].join(":"),
|
|
898
|
+
kind: telephonyFrameKind(direction),
|
|
899
|
+
metadata: {
|
|
900
|
+
carrier,
|
|
901
|
+
direction,
|
|
902
|
+
event: firstString([envelope], ["event", "type"]),
|
|
903
|
+
sequenceNumber,
|
|
904
|
+
streamId,
|
|
905
|
+
track
|
|
906
|
+
},
|
|
907
|
+
sessionId: input.sessionId ?? streamId,
|
|
908
|
+
source: "telephony"
|
|
909
|
+
};
|
|
910
|
+
};
|
|
911
|
+
var serializeTelephonyMediaFrame = (input) => {
|
|
912
|
+
const carrier = input.carrier ?? input.frame.metadata?.carrier ?? "telephony";
|
|
913
|
+
const streamId = input.streamId ?? (typeof input.frame.metadata?.streamId === "string" ? input.frame.metadata.streamId : input.frame.sessionId);
|
|
914
|
+
const sequenceNumber = input.sequenceNumber ?? (typeof input.frame.metadata?.sequenceNumber === "string" || typeof input.frame.metadata?.sequenceNumber === "number" ? input.frame.metadata.sequenceNumber : undefined);
|
|
915
|
+
const direction = input.frame.kind === "assistant-audio" ? "outbound" : "inbound";
|
|
916
|
+
const payload = input.frame.audio ? bytesToBase64(input.frame.audio) : "";
|
|
917
|
+
if (carrier === "twilio") {
|
|
918
|
+
return {
|
|
919
|
+
event: "media",
|
|
920
|
+
sequenceNumber,
|
|
921
|
+
streamSid: streamId,
|
|
922
|
+
media: {
|
|
923
|
+
payload,
|
|
924
|
+
timestamp: input.frame.at,
|
|
925
|
+
track: direction
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (carrier === "telnyx") {
|
|
930
|
+
return {
|
|
931
|
+
event: "media",
|
|
932
|
+
stream_id: streamId,
|
|
933
|
+
sequence_number: sequenceNumber,
|
|
934
|
+
media: {
|
|
935
|
+
payload,
|
|
936
|
+
timestamp: input.frame.at,
|
|
937
|
+
track: direction
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (carrier === "plivo") {
|
|
942
|
+
return {
|
|
943
|
+
event: "media",
|
|
944
|
+
streamId,
|
|
945
|
+
sequenceNumber,
|
|
946
|
+
media: {
|
|
947
|
+
payload,
|
|
948
|
+
timestamp: input.frame.at,
|
|
949
|
+
track: direction
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
event: "media",
|
|
955
|
+
provider: carrier,
|
|
956
|
+
sequenceNumber,
|
|
957
|
+
streamId,
|
|
958
|
+
media: {
|
|
959
|
+
payload,
|
|
960
|
+
timestamp: input.frame.at,
|
|
961
|
+
track: direction
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
};
|
|
965
|
+
var createTelephonyMediaSerializer = (input) => {
|
|
966
|
+
const format = input.format ?? DEFAULT_TELEPHONY_FORMAT;
|
|
967
|
+
return {
|
|
968
|
+
carrier: input.carrier,
|
|
969
|
+
format,
|
|
970
|
+
parse: (envelope) => parseTelephonyMediaFrame({
|
|
971
|
+
carrier: input.carrier,
|
|
972
|
+
envelope,
|
|
973
|
+
format,
|
|
974
|
+
sessionId: input.sessionId ?? input.streamId
|
|
975
|
+
}),
|
|
976
|
+
serialize: (frame) => serializeTelephonyMediaFrame({
|
|
977
|
+
carrier: input.carrier,
|
|
978
|
+
frame,
|
|
979
|
+
streamId: input.streamId
|
|
980
|
+
})
|
|
981
|
+
};
|
|
982
|
+
};
|
|
818
983
|
var buildMediaResamplingPlan = (input) => {
|
|
819
984
|
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
820
985
|
return {
|
|
@@ -1051,12 +1216,64 @@ var collectMediaWebRTCStats = async (input) => {
|
|
|
1051
1216
|
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
1052
1217
|
return [...report.values()].map(normalizeWebRTCStat);
|
|
1053
1218
|
};
|
|
1054
|
-
var
|
|
1055
|
-
const stats =
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1219
|
+
var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
|
|
1220
|
+
const stats = input.stats ?? [];
|
|
1221
|
+
const previousStats = input.previousStats ?? [];
|
|
1222
|
+
const issues = [];
|
|
1223
|
+
const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
|
|
1224
|
+
const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
|
|
1225
|
+
const streams = audioRtp.map((stat) => {
|
|
1226
|
+
const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
|
|
1227
|
+
const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
|
|
1228
|
+
const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
|
|
1229
|
+
const previous = previousByKey.get(statKey(stat));
|
|
1230
|
+
const currentPackets = numericStat(stat, packetsKey);
|
|
1231
|
+
const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
|
|
1232
|
+
const currentBytes = numericStat(stat, bytesKey);
|
|
1233
|
+
const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
|
|
1234
|
+
const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
|
|
1235
|
+
return {
|
|
1236
|
+
bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
|
|
1237
|
+
currentPackets,
|
|
1238
|
+
direction,
|
|
1239
|
+
id: statKey(stat),
|
|
1240
|
+
packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
|
|
1241
|
+
previousPackets,
|
|
1242
|
+
timeDeltaMs
|
|
1243
|
+
};
|
|
1059
1244
|
});
|
|
1245
|
+
const inbound = streams.filter((stream) => stream.direction === "inbound");
|
|
1246
|
+
const outbound = streams.filter((stream) => stream.direction === "outbound");
|
|
1247
|
+
const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
|
|
1248
|
+
const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
1249
|
+
const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
1250
|
+
if (input.requireInboundAudio && inbound.length === 0) {
|
|
1251
|
+
pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
|
|
1252
|
+
}
|
|
1253
|
+
if (input.requireOutboundAudio && outbound.length === 0) {
|
|
1254
|
+
pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
|
|
1255
|
+
}
|
|
1256
|
+
if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
|
|
1257
|
+
pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
|
|
1258
|
+
}
|
|
1259
|
+
if (stalledInboundStreams > 0) {
|
|
1260
|
+
pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
|
|
1261
|
+
}
|
|
1262
|
+
if (stalledOutboundStreams > 0) {
|
|
1263
|
+
pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
checkedAt: Date.now(),
|
|
1267
|
+
inboundAudioStreams: inbound.length,
|
|
1268
|
+
issues,
|
|
1269
|
+
maxObservedGapMs,
|
|
1270
|
+
outboundAudioStreams: outbound.length,
|
|
1271
|
+
stalledInboundStreams,
|
|
1272
|
+
stalledOutboundStreams,
|
|
1273
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
1274
|
+
streams,
|
|
1275
|
+
totalStats: stats.length
|
|
1276
|
+
};
|
|
1060
1277
|
};
|
|
1061
1278
|
var buildMediaPipelineCalibrationReport = (input = {}) => {
|
|
1062
1279
|
const frames = input.frames ?? [];
|
|
@@ -1142,21 +1359,30 @@ var postBrowserMediaReport = async (payload, options) => {
|
|
|
1142
1359
|
};
|
|
1143
1360
|
var createVoiceBrowserMediaReporter = (options) => {
|
|
1144
1361
|
let interval = null;
|
|
1362
|
+
let previousStats = [];
|
|
1145
1363
|
const reportOnce = async () => {
|
|
1146
1364
|
const peerConnection = await resolvePeerConnection(options);
|
|
1147
1365
|
if (!peerConnection) {
|
|
1148
1366
|
return;
|
|
1149
1367
|
}
|
|
1150
|
-
const
|
|
1368
|
+
const stats = await collectMediaWebRTCStats({ peerConnection });
|
|
1369
|
+
const report = buildMediaWebRTCStatsReport({
|
|
1151
1370
|
...options,
|
|
1152
|
-
|
|
1371
|
+
stats
|
|
1372
|
+
});
|
|
1373
|
+
const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
|
|
1374
|
+
...options.continuity,
|
|
1375
|
+
previousStats,
|
|
1376
|
+
stats
|
|
1153
1377
|
});
|
|
1154
1378
|
const payload = {
|
|
1155
1379
|
at: Date.now(),
|
|
1380
|
+
continuity,
|
|
1156
1381
|
report,
|
|
1157
1382
|
scenarioId: options.getScenarioId?.() ?? null,
|
|
1158
1383
|
sessionId: options.getSessionId?.() ?? null
|
|
1159
1384
|
};
|
|
1385
|
+
previousStats = stats;
|
|
1160
1386
|
options.onReport?.(payload);
|
|
1161
1387
|
await postBrowserMediaReport(payload, options);
|
|
1162
1388
|
return payload;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,10 @@ export { assertVoiceRealtimeProviderContractEvidence, buildVoiceRealtimeProvider
|
|
|
16
16
|
export type { VoiceRealtimeProviderContractAssertionInput, VoiceRealtimeProviderContractAssertionReport, VoiceRealtimeProviderContractCapability, VoiceRealtimeProviderContractCheck, VoiceRealtimeProviderContractDefinition, VoiceRealtimeProviderContractMatrixPresetOptions, VoiceRealtimeProviderContractMatrixInput, VoiceRealtimeProviderContractMatrixReport, VoiceRealtimeProviderContractRoutesOptions, VoiceRealtimeProviderContractRow, VoiceRealtimeProviderPresetProvider, VoiceRealtimeProviderContractStatus } from './realtimeProviderContracts';
|
|
17
17
|
export { buildVoiceDiagnosticsMarkdown, createVoiceDiagnosticsRoutes, resolveVoiceDiagnosticsTraceFilter } from './diagnosticsRoutes';
|
|
18
18
|
export { assertVoiceMediaPipelineEvidence, buildVoiceMediaPipelineReport, createVoiceMediaPipelineRoutes, evaluateVoiceMediaPipelineEvidence, renderVoiceMediaPipelineHTML, renderVoiceMediaPipelineMarkdown } from './mediaPipelineRoutes';
|
|
19
|
+
export { buildVoiceTelephonyMediaReport, createVoiceTelephonyMediaRoutes, renderVoiceTelephonyMediaHTML } from './telephonyMediaRoutes';
|
|
19
20
|
export { createVoiceBrowserMediaRoutes, getLatestVoiceBrowserMediaReport, renderVoiceBrowserMediaHTML, summarizeVoiceBrowserMedia } from './browserMediaRoutes';
|
|
20
21
|
export type { VoiceMediaPipelineAssertionInput, VoiceMediaPipelineAssertionReport, VoiceMediaPipelineReport, VoiceMediaPipelineReportOptions, VoiceMediaPipelineRoutesOptions } from './mediaPipelineRoutes';
|
|
22
|
+
export type { VoiceTelephonyMediaCarrierInput, VoiceTelephonyMediaCarrierReport, VoiceTelephonyMediaReport, VoiceTelephonyMediaRoutesOptions, VoiceTelephonyMediaStatus } from './telephonyMediaRoutes';
|
|
21
23
|
export type { VoiceBrowserMediaReport, VoiceBrowserMediaRoutesOptions, VoiceBrowserMediaSample, VoiceBrowserMediaStatus } from './browserMediaRoutes';
|
|
22
24
|
export { buildVoiceDemoReadyReport, createVoiceDemoReadyRoutes, renderVoiceDemoReadyHTML } from './demoReadyRoutes';
|
|
23
25
|
export { buildVoiceDeliverySinkReport, createVoiceDeliverySinkDescriptor, createVoiceDeliverySinkPair, createVoiceDeliverySinkRoutes, createVoiceFileDeliverySink, createVoicePostgresDeliverySink, createVoiceS3DeliverySink, createVoiceSQLiteDeliverySink, createVoiceWebhookDeliverySink, renderVoiceDeliverySinkHTML } from './deliverySinkRoutes';
|