@absolutejs/voice 0.0.22-beta.324 → 0.0.22-beta.325
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 +103 -0
- package/dist/client/index.js +103 -0
- package/dist/index.js +162 -6
- package/dist/productionReadiness.d.ts +2 -0
- package/dist/react/index.js +103 -0
- package/dist/svelte/index.js +103 -0
- package/dist/telephonyMediaRoutes.d.ts +3 -1
- package/dist/testing/index.js +103 -0
- package/dist/vue/index.js +103 -0
- package/dist/vue/useVoiceReadinessFailures.d.ts +4 -0
- package/package.json +2 -2
package/dist/angular/index.js
CHANGED
|
@@ -1471,6 +1471,29 @@ var telephonyDirection = (track) => {
|
|
|
1471
1471
|
return "unknown";
|
|
1472
1472
|
};
|
|
1473
1473
|
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
1474
|
+
var telephonyEventKind = (envelope) => {
|
|
1475
|
+
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
1476
|
+
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
1477
|
+
if (!normalized) {
|
|
1478
|
+
return "unknown";
|
|
1479
|
+
}
|
|
1480
|
+
if (normalized.includes("connected")) {
|
|
1481
|
+
return "connected";
|
|
1482
|
+
}
|
|
1483
|
+
if (normalized.includes("start")) {
|
|
1484
|
+
return "start";
|
|
1485
|
+
}
|
|
1486
|
+
if (normalized.includes("media")) {
|
|
1487
|
+
return "media";
|
|
1488
|
+
}
|
|
1489
|
+
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
1490
|
+
return "stop";
|
|
1491
|
+
}
|
|
1492
|
+
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
1493
|
+
return "error";
|
|
1494
|
+
}
|
|
1495
|
+
return "unknown";
|
|
1496
|
+
};
|
|
1474
1497
|
var normalizeWebRTCStat = (stat) => {
|
|
1475
1498
|
const sample = {};
|
|
1476
1499
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -1587,6 +1610,86 @@ var createTelephonyMediaSerializer = (input) => {
|
|
|
1587
1610
|
})
|
|
1588
1611
|
};
|
|
1589
1612
|
};
|
|
1613
|
+
var parseTelephonyStreamEvent = (input) => {
|
|
1614
|
+
const envelope = input.envelope;
|
|
1615
|
+
const media = unknownRecord(envelope.media);
|
|
1616
|
+
const start = unknownRecord(envelope.start);
|
|
1617
|
+
const stop = unknownRecord(envelope.stop);
|
|
1618
|
+
const errorRecord = unknownRecord(envelope.error);
|
|
1619
|
+
const kind = telephonyEventKind(envelope);
|
|
1620
|
+
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
1621
|
+
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
1622
|
+
carrier,
|
|
1623
|
+
envelope,
|
|
1624
|
+
format: input.format,
|
|
1625
|
+
sessionId: input.sessionId
|
|
1626
|
+
}) : undefined;
|
|
1627
|
+
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
1628
|
+
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
1629
|
+
const track = firstString([media, envelope], ["track", "direction"]);
|
|
1630
|
+
return {
|
|
1631
|
+
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
1632
|
+
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
1633
|
+
carrier,
|
|
1634
|
+
direction: telephonyDirection(track),
|
|
1635
|
+
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
1636
|
+
kind,
|
|
1637
|
+
sequenceNumber,
|
|
1638
|
+
streamId
|
|
1639
|
+
};
|
|
1640
|
+
};
|
|
1641
|
+
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
1642
|
+
const envelopes = input.envelopes ?? [];
|
|
1643
|
+
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
1644
|
+
carrier: input.carrier,
|
|
1645
|
+
envelope
|
|
1646
|
+
}));
|
|
1647
|
+
const issues = [];
|
|
1648
|
+
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
1649
|
+
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
1650
|
+
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
1651
|
+
const started = startedIndex >= 0;
|
|
1652
|
+
const stopped = stoppedIndex >= 0;
|
|
1653
|
+
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
1654
|
+
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
1655
|
+
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
1656
|
+
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
1657
|
+
if ((input.requireStart ?? true) && !started) {
|
|
1658
|
+
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
1659
|
+
}
|
|
1660
|
+
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
1661
|
+
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
1662
|
+
}
|
|
1663
|
+
if ((input.requireStop ?? true) && !stopped) {
|
|
1664
|
+
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
1665
|
+
}
|
|
1666
|
+
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
1667
|
+
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
1668
|
+
}
|
|
1669
|
+
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
1670
|
+
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
1671
|
+
}
|
|
1672
|
+
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
1673
|
+
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
1674
|
+
}
|
|
1675
|
+
for (const event of events) {
|
|
1676
|
+
if (event.kind === "error") {
|
|
1677
|
+
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return {
|
|
1681
|
+
audioBytes,
|
|
1682
|
+
carrier: input.carrier,
|
|
1683
|
+
checkedAt: Date.now(),
|
|
1684
|
+
events,
|
|
1685
|
+
issues,
|
|
1686
|
+
mediaEvents: mediaEvents.length,
|
|
1687
|
+
started,
|
|
1688
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
1689
|
+
stopped,
|
|
1690
|
+
streamIds
|
|
1691
|
+
};
|
|
1692
|
+
};
|
|
1590
1693
|
var buildMediaResamplingPlan = (input) => {
|
|
1591
1694
|
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
1592
1695
|
return {
|
package/dist/client/index.js
CHANGED
|
@@ -864,6 +864,29 @@ var telephonyDirection = (track) => {
|
|
|
864
864
|
return "unknown";
|
|
865
865
|
};
|
|
866
866
|
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
867
|
+
var telephonyEventKind = (envelope) => {
|
|
868
|
+
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
869
|
+
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
870
|
+
if (!normalized) {
|
|
871
|
+
return "unknown";
|
|
872
|
+
}
|
|
873
|
+
if (normalized.includes("connected")) {
|
|
874
|
+
return "connected";
|
|
875
|
+
}
|
|
876
|
+
if (normalized.includes("start")) {
|
|
877
|
+
return "start";
|
|
878
|
+
}
|
|
879
|
+
if (normalized.includes("media")) {
|
|
880
|
+
return "media";
|
|
881
|
+
}
|
|
882
|
+
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
883
|
+
return "stop";
|
|
884
|
+
}
|
|
885
|
+
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
886
|
+
return "error";
|
|
887
|
+
}
|
|
888
|
+
return "unknown";
|
|
889
|
+
};
|
|
867
890
|
var normalizeWebRTCStat = (stat) => {
|
|
868
891
|
const sample = {};
|
|
869
892
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -980,6 +1003,86 @@ var createTelephonyMediaSerializer = (input) => {
|
|
|
980
1003
|
})
|
|
981
1004
|
};
|
|
982
1005
|
};
|
|
1006
|
+
var parseTelephonyStreamEvent = (input) => {
|
|
1007
|
+
const envelope = input.envelope;
|
|
1008
|
+
const media = unknownRecord(envelope.media);
|
|
1009
|
+
const start = unknownRecord(envelope.start);
|
|
1010
|
+
const stop = unknownRecord(envelope.stop);
|
|
1011
|
+
const errorRecord = unknownRecord(envelope.error);
|
|
1012
|
+
const kind = telephonyEventKind(envelope);
|
|
1013
|
+
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
1014
|
+
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
1015
|
+
carrier,
|
|
1016
|
+
envelope,
|
|
1017
|
+
format: input.format,
|
|
1018
|
+
sessionId: input.sessionId
|
|
1019
|
+
}) : undefined;
|
|
1020
|
+
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
1021
|
+
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
1022
|
+
const track = firstString([media, envelope], ["track", "direction"]);
|
|
1023
|
+
return {
|
|
1024
|
+
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
1025
|
+
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
1026
|
+
carrier,
|
|
1027
|
+
direction: telephonyDirection(track),
|
|
1028
|
+
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
1029
|
+
kind,
|
|
1030
|
+
sequenceNumber,
|
|
1031
|
+
streamId
|
|
1032
|
+
};
|
|
1033
|
+
};
|
|
1034
|
+
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
1035
|
+
const envelopes = input.envelopes ?? [];
|
|
1036
|
+
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
1037
|
+
carrier: input.carrier,
|
|
1038
|
+
envelope
|
|
1039
|
+
}));
|
|
1040
|
+
const issues = [];
|
|
1041
|
+
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
1042
|
+
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
1043
|
+
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
1044
|
+
const started = startedIndex >= 0;
|
|
1045
|
+
const stopped = stoppedIndex >= 0;
|
|
1046
|
+
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
1047
|
+
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
1048
|
+
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
1049
|
+
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
1050
|
+
if ((input.requireStart ?? true) && !started) {
|
|
1051
|
+
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
1052
|
+
}
|
|
1053
|
+
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
1054
|
+
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
1055
|
+
}
|
|
1056
|
+
if ((input.requireStop ?? true) && !stopped) {
|
|
1057
|
+
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
1058
|
+
}
|
|
1059
|
+
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
1060
|
+
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
1061
|
+
}
|
|
1062
|
+
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
1063
|
+
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
1064
|
+
}
|
|
1065
|
+
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
1066
|
+
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
1067
|
+
}
|
|
1068
|
+
for (const event of events) {
|
|
1069
|
+
if (event.kind === "error") {
|
|
1070
|
+
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
audioBytes,
|
|
1075
|
+
carrier: input.carrier,
|
|
1076
|
+
checkedAt: Date.now(),
|
|
1077
|
+
events,
|
|
1078
|
+
issues,
|
|
1079
|
+
mediaEvents: mediaEvents.length,
|
|
1080
|
+
started,
|
|
1081
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
1082
|
+
stopped,
|
|
1083
|
+
streamIds
|
|
1084
|
+
};
|
|
1085
|
+
};
|
|
983
1086
|
var buildMediaResamplingPlan = (input) => {
|
|
984
1087
|
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
985
1088
|
return {
|
package/dist/index.js
CHANGED
|
@@ -11696,6 +11696,29 @@ var telephonyDirection = (track) => {
|
|
|
11696
11696
|
return "unknown";
|
|
11697
11697
|
};
|
|
11698
11698
|
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
11699
|
+
var telephonyEventKind = (envelope) => {
|
|
11700
|
+
const raw = firstString2([envelope], ["event", "type", "eventType"]) ?? firstString2([unknownRecord(envelope.message)], ["event", "type"]);
|
|
11701
|
+
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
11702
|
+
if (!normalized) {
|
|
11703
|
+
return "unknown";
|
|
11704
|
+
}
|
|
11705
|
+
if (normalized.includes("connected")) {
|
|
11706
|
+
return "connected";
|
|
11707
|
+
}
|
|
11708
|
+
if (normalized.includes("start")) {
|
|
11709
|
+
return "start";
|
|
11710
|
+
}
|
|
11711
|
+
if (normalized.includes("media")) {
|
|
11712
|
+
return "media";
|
|
11713
|
+
}
|
|
11714
|
+
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
11715
|
+
return "stop";
|
|
11716
|
+
}
|
|
11717
|
+
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
11718
|
+
return "error";
|
|
11719
|
+
}
|
|
11720
|
+
return "unknown";
|
|
11721
|
+
};
|
|
11699
11722
|
var normalizeWebRTCStat = (stat) => {
|
|
11700
11723
|
const sample = {};
|
|
11701
11724
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -11812,6 +11835,86 @@ var createTelephonyMediaSerializer = (input) => {
|
|
|
11812
11835
|
})
|
|
11813
11836
|
};
|
|
11814
11837
|
};
|
|
11838
|
+
var parseTelephonyStreamEvent = (input) => {
|
|
11839
|
+
const envelope = input.envelope;
|
|
11840
|
+
const media = unknownRecord(envelope.media);
|
|
11841
|
+
const start = unknownRecord(envelope.start);
|
|
11842
|
+
const stop = unknownRecord(envelope.stop);
|
|
11843
|
+
const errorRecord = unknownRecord(envelope.error);
|
|
11844
|
+
const kind = telephonyEventKind(envelope);
|
|
11845
|
+
const carrier = input.carrier ?? firstString2([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
11846
|
+
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
11847
|
+
carrier,
|
|
11848
|
+
envelope,
|
|
11849
|
+
format: input.format,
|
|
11850
|
+
sessionId: input.sessionId
|
|
11851
|
+
}) : undefined;
|
|
11852
|
+
const streamId = firstString2([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
11853
|
+
const sequenceNumber = firstString2([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
11854
|
+
const track = firstString2([media, envelope], ["track", "direction"]);
|
|
11855
|
+
return {
|
|
11856
|
+
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
11857
|
+
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
11858
|
+
carrier,
|
|
11859
|
+
direction: telephonyDirection(track),
|
|
11860
|
+
error: firstString2([errorRecord, envelope], ["message", "error", "reason"]),
|
|
11861
|
+
kind,
|
|
11862
|
+
sequenceNumber,
|
|
11863
|
+
streamId
|
|
11864
|
+
};
|
|
11865
|
+
};
|
|
11866
|
+
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
11867
|
+
const envelopes = input.envelopes ?? [];
|
|
11868
|
+
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
11869
|
+
carrier: input.carrier,
|
|
11870
|
+
envelope
|
|
11871
|
+
}));
|
|
11872
|
+
const issues = [];
|
|
11873
|
+
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
11874
|
+
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
11875
|
+
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
11876
|
+
const started = startedIndex >= 0;
|
|
11877
|
+
const stopped = stoppedIndex >= 0;
|
|
11878
|
+
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
11879
|
+
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
11880
|
+
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
11881
|
+
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
11882
|
+
if ((input.requireStart ?? true) && !started) {
|
|
11883
|
+
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
11884
|
+
}
|
|
11885
|
+
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
11886
|
+
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
11887
|
+
}
|
|
11888
|
+
if ((input.requireStop ?? true) && !stopped) {
|
|
11889
|
+
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
11890
|
+
}
|
|
11891
|
+
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
11892
|
+
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
11893
|
+
}
|
|
11894
|
+
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
11895
|
+
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
11896
|
+
}
|
|
11897
|
+
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
11898
|
+
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
11899
|
+
}
|
|
11900
|
+
for (const event of events) {
|
|
11901
|
+
if (event.kind === "error") {
|
|
11902
|
+
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
11903
|
+
}
|
|
11904
|
+
}
|
|
11905
|
+
return {
|
|
11906
|
+
audioBytes,
|
|
11907
|
+
carrier: input.carrier,
|
|
11908
|
+
checkedAt: Date.now(),
|
|
11909
|
+
events,
|
|
11910
|
+
issues,
|
|
11911
|
+
mediaEvents: mediaEvents.length,
|
|
11912
|
+
started,
|
|
11913
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
11914
|
+
stopped,
|
|
11915
|
+
streamIds
|
|
11916
|
+
};
|
|
11917
|
+
};
|
|
11815
11918
|
var buildMediaResamplingPlan = (input) => {
|
|
11816
11919
|
const required = !formatMatches2(input.inputFormat, input.outputFormat);
|
|
11817
11920
|
return {
|
|
@@ -12453,6 +12556,49 @@ var demoEnvelope = (carrier) => {
|
|
|
12453
12556
|
streamId: "proof-plivo-media"
|
|
12454
12557
|
};
|
|
12455
12558
|
};
|
|
12559
|
+
var demoLifecycleEnvelopes = (carrier) => {
|
|
12560
|
+
if (carrier === "twilio") {
|
|
12561
|
+
return [
|
|
12562
|
+
{
|
|
12563
|
+
event: "start",
|
|
12564
|
+
start: {
|
|
12565
|
+
streamSid: "proof-twilio-media"
|
|
12566
|
+
}
|
|
12567
|
+
},
|
|
12568
|
+
demoEnvelope(carrier),
|
|
12569
|
+
{
|
|
12570
|
+
event: "stop",
|
|
12571
|
+
stop: {
|
|
12572
|
+
streamSid: "proof-twilio-media"
|
|
12573
|
+
}
|
|
12574
|
+
}
|
|
12575
|
+
];
|
|
12576
|
+
}
|
|
12577
|
+
if (carrier === "telnyx") {
|
|
12578
|
+
return [
|
|
12579
|
+
{
|
|
12580
|
+
event: "start",
|
|
12581
|
+
stream_id: "proof-telnyx-media"
|
|
12582
|
+
},
|
|
12583
|
+
demoEnvelope(carrier),
|
|
12584
|
+
{
|
|
12585
|
+
event: "stop",
|
|
12586
|
+
stream_id: "proof-telnyx-media"
|
|
12587
|
+
}
|
|
12588
|
+
];
|
|
12589
|
+
}
|
|
12590
|
+
return [
|
|
12591
|
+
{
|
|
12592
|
+
event: "start",
|
|
12593
|
+
streamId: "proof-plivo-media"
|
|
12594
|
+
},
|
|
12595
|
+
demoEnvelope(carrier),
|
|
12596
|
+
{
|
|
12597
|
+
event: "stop",
|
|
12598
|
+
streamId: "proof-plivo-media"
|
|
12599
|
+
}
|
|
12600
|
+
];
|
|
12601
|
+
};
|
|
12456
12602
|
var byteLength = (audio) => {
|
|
12457
12603
|
if (!audio) {
|
|
12458
12604
|
return 0;
|
|
@@ -12478,6 +12624,10 @@ var buildVoiceTelephonyMediaReport = (input = {}) => {
|
|
|
12478
12624
|
carrier: entry.carrier,
|
|
12479
12625
|
frame
|
|
12480
12626
|
}) : undefined;
|
|
12627
|
+
const lifecycle = buildMediaTelephonyStreamLifecycleReport({
|
|
12628
|
+
carrier: entry.carrier,
|
|
12629
|
+
envelopes: entry.lifecycleEnvelopes ?? demoLifecycleEnvelopes(entry.carrier)
|
|
12630
|
+
});
|
|
12481
12631
|
const audioBytes = byteLength(frame?.audio);
|
|
12482
12632
|
const issues2 = [];
|
|
12483
12633
|
if (!frame) {
|
|
@@ -12495,11 +12645,15 @@ var buildVoiceTelephonyMediaReport = (input = {}) => {
|
|
|
12495
12645
|
if (!serialized || typeof serialized !== "object") {
|
|
12496
12646
|
issues2.push("MediaFrame did not serialize back into a carrier envelope.");
|
|
12497
12647
|
}
|
|
12648
|
+
for (const issue of lifecycle.issues) {
|
|
12649
|
+
issues2.push(issue.message);
|
|
12650
|
+
}
|
|
12498
12651
|
return {
|
|
12499
12652
|
audioBytes,
|
|
12500
12653
|
carrier: entry.carrier,
|
|
12501
12654
|
frame,
|
|
12502
12655
|
issues: issues2,
|
|
12656
|
+
lifecycle,
|
|
12503
12657
|
serialized,
|
|
12504
12658
|
status: issues2.length === 0 ? "pass" : "fail"
|
|
12505
12659
|
};
|
|
@@ -12514,8 +12668,8 @@ var buildVoiceTelephonyMediaReport = (input = {}) => {
|
|
|
12514
12668
|
};
|
|
12515
12669
|
var renderVoiceTelephonyMediaHTML = (report, options = {}) => {
|
|
12516
12670
|
const title = options.title ?? "Voice Telephony Media Proof";
|
|
12517
|
-
const rows = report.carriers.map((carrier) => `<tr><td>${escapeHtml16(carrier.carrier)}</td><td>${escapeHtml16(carrier.status)}</td><td>${String(carrier.audioBytes)}</td><td>${escapeHtml16(carrier.frame?.kind ?? "missing")}</td><td>${escapeHtml16(carrier.frame?.format?.encoding ?? "missing")}</td><td>${escapeHtml16(carrier.issues.join(" ") || "none")}</td></tr>`).join("");
|
|
12518
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(title)}</title><style>body{background:#111827;color:#e5e7eb;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:980px;padding:32px}.hero,table{background:#0f172a;border:1px solid #334155;border-radius:20px;margin-bottom:16px}.hero{padding:22px}.eyebrow{color:#67e8f9;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.5rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #64748b;border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.pass{color:#86efac}.fail{color:#fecaca}code{color:#bfdbfe}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:10px;text-align:left}</style></head><body><main><section class="hero"><p class="eyebrow">Carrier media serializer proof</p><h1>${escapeHtml16(title)}</h1><p class="status ${escapeHtml16(report.status)}">Status: ${escapeHtml16(report.status)}</p><p>Twilio, Telnyx, and Plivo media payload envelopes are parsed into generic <code>MediaFrame</code> objects
|
|
12671
|
+
const rows = report.carriers.map((carrier) => `<tr><td>${escapeHtml16(carrier.carrier)}</td><td>${escapeHtml16(carrier.status)}</td><td>${String(carrier.audioBytes)}</td><td>${String(carrier.lifecycle.mediaEvents)}</td><td>${escapeHtml16(carrier.lifecycle.started ? "yes" : "no")}</td><td>${escapeHtml16(carrier.lifecycle.stopped ? "yes" : "no")}</td><td>${escapeHtml16(carrier.frame?.kind ?? "missing")}</td><td>${escapeHtml16(carrier.frame?.format?.encoding ?? "missing")}</td><td>${escapeHtml16(carrier.issues.join(" ") || "none")}</td></tr>`).join("");
|
|
12672
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(title)}</title><style>body{background:#111827;color:#e5e7eb;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:980px;padding:32px}.hero,table{background:#0f172a;border:1px solid #334155;border-radius:20px;margin-bottom:16px}.hero{padding:22px}.eyebrow{color:#67e8f9;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.5rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #64748b;border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.pass{color:#86efac}.fail{color:#fecaca}code{color:#bfdbfe}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:10px;text-align:left}</style></head><body><main><section class="hero"><p class="eyebrow">Carrier media serializer proof</p><h1>${escapeHtml16(title)}</h1><p class="status ${escapeHtml16(report.status)}">Status: ${escapeHtml16(report.status)}</p><p>Twilio, Telnyx, and Plivo media payload envelopes are parsed into generic <code>MediaFrame</code> objects, serialized back into carrier envelopes, and checked for start/media/stop lifecycle sequencing by <code>@absolutejs/media</code>.</p></section><table><thead><tr><th>Carrier</th><th>Status</th><th>Audio bytes</th><th>Media events</th><th>Started</th><th>Stopped</th><th>Frame kind</th><th>Encoding</th><th>Issues</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
|
|
12519
12673
|
};
|
|
12520
12674
|
var createVoiceTelephonyMediaRoutes = (options = {}) => {
|
|
12521
12675
|
const path = options.path ?? "/api/voice/telephony/media";
|
|
@@ -30416,6 +30570,8 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
30416
30570
|
carriers: telephonyMedia.carriers.length,
|
|
30417
30571
|
failed: telephonyMedia.carriers.filter((carrier) => carrier.status !== "pass").length,
|
|
30418
30572
|
issues: telephonyMedia.issues.length,
|
|
30573
|
+
lifecycleFailures: telephonyMedia.carriers.filter((carrier) => carrier.lifecycle.status !== "pass").length,
|
|
30574
|
+
mediaEvents: telephonyMedia.carriers.reduce((total, carrier) => total + carrier.lifecycle.mediaEvents, 0),
|
|
30419
30575
|
passed: telephonyMedia.carriers.filter((carrier) => carrier.status === "pass").length,
|
|
30420
30576
|
status: telephonyMedia.status === "pass" ? "pass" : "fail"
|
|
30421
30577
|
} : undefined;
|
|
@@ -30514,22 +30670,22 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
30514
30670
|
if (telephonyMedia && telephonyMediaSummary) {
|
|
30515
30671
|
const firstIssue = telephonyMedia.issues[0];
|
|
30516
30672
|
checks.push({
|
|
30517
|
-
detail: telephonyMediaSummary.status === "pass" ? `Telephony media serializers are passing for ${telephonyMediaSummary.passed}/${telephonyMediaSummary.carriers} carrier(s) with ${telephonyMediaSummary.audioBytes} audio byte(s)
|
|
30673
|
+
detail: telephonyMediaSummary.status === "pass" ? `Telephony media serializers are passing for ${telephonyMediaSummary.passed}/${telephonyMediaSummary.carriers} carrier(s) with ${telephonyMediaSummary.audioBytes} audio byte(s), ${telephonyMediaSummary.mediaEvents} media event(s), and valid start/media/stop lifecycle sequencing.` : firstIssue ?? `${telephonyMediaSummary.issues} telephony media serializer issue(s) need review.`,
|
|
30518
30674
|
href: options.links?.telephonyMedia ?? "/voice/telephony-media",
|
|
30519
30675
|
label: "Telephony media serializers",
|
|
30520
30676
|
proofSource: proofSource("telephonyMedia", "carrierMediaSerializers"),
|
|
30521
30677
|
gateExplanation: telephonyMediaSummary.status === "pass" ? undefined : {
|
|
30522
30678
|
evidenceHref: options.links?.telephonyMedia ?? "/voice/telephony-media",
|
|
30523
30679
|
observed: firstIssue ?? `${telephonyMediaSummary.issues} issue(s)`,
|
|
30524
|
-
remediation: "Inspect carrier media
|
|
30525
|
-
thresholdLabel: "Telephony media serializer status",
|
|
30680
|
+
remediation: "Inspect carrier media proof, fix start/media/stop sequencing, payload parsing, byte flow, or outbound envelope serialization, then rerun readiness proof.",
|
|
30681
|
+
thresholdLabel: "Telephony media serializer and lifecycle status",
|
|
30526
30682
|
unit: "status"
|
|
30527
30683
|
},
|
|
30528
30684
|
status: telephonyMediaSummary.status,
|
|
30529
30685
|
value: telephonyMediaSummary.status === "pass" ? `${telephonyMediaSummary.passed}/${telephonyMediaSummary.carriers}` : `${telephonyMediaSummary.failed}/${telephonyMediaSummary.carriers} failing`,
|
|
30530
30686
|
actions: telephonyMediaSummary.status === "pass" ? [] : [
|
|
30531
30687
|
{
|
|
30532
|
-
description: "Open telephony media proof and inspect carrier media payload parsing, MediaFrame shape, and outbound envelope serialization.",
|
|
30688
|
+
description: "Open telephony media proof and inspect carrier media lifecycle sequencing, payload parsing, MediaFrame shape, byte flow, and outbound envelope serialization.",
|
|
30533
30689
|
href: options.links?.telephonyMedia ?? "/voice/telephony-media",
|
|
30534
30690
|
label: "Open telephony media proof"
|
|
30535
30691
|
}
|
package/dist/react/index.js
CHANGED
|
@@ -5107,6 +5107,29 @@ var telephonyDirection = (track) => {
|
|
|
5107
5107
|
return "unknown";
|
|
5108
5108
|
};
|
|
5109
5109
|
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
5110
|
+
var telephonyEventKind = (envelope) => {
|
|
5111
|
+
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
5112
|
+
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
5113
|
+
if (!normalized) {
|
|
5114
|
+
return "unknown";
|
|
5115
|
+
}
|
|
5116
|
+
if (normalized.includes("connected")) {
|
|
5117
|
+
return "connected";
|
|
5118
|
+
}
|
|
5119
|
+
if (normalized.includes("start")) {
|
|
5120
|
+
return "start";
|
|
5121
|
+
}
|
|
5122
|
+
if (normalized.includes("media")) {
|
|
5123
|
+
return "media";
|
|
5124
|
+
}
|
|
5125
|
+
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
5126
|
+
return "stop";
|
|
5127
|
+
}
|
|
5128
|
+
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
5129
|
+
return "error";
|
|
5130
|
+
}
|
|
5131
|
+
return "unknown";
|
|
5132
|
+
};
|
|
5110
5133
|
var normalizeWebRTCStat = (stat) => {
|
|
5111
5134
|
const sample = {};
|
|
5112
5135
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -5223,6 +5246,86 @@ var createTelephonyMediaSerializer = (input) => {
|
|
|
5223
5246
|
})
|
|
5224
5247
|
};
|
|
5225
5248
|
};
|
|
5249
|
+
var parseTelephonyStreamEvent = (input) => {
|
|
5250
|
+
const envelope = input.envelope;
|
|
5251
|
+
const media = unknownRecord(envelope.media);
|
|
5252
|
+
const start = unknownRecord(envelope.start);
|
|
5253
|
+
const stop = unknownRecord(envelope.stop);
|
|
5254
|
+
const errorRecord = unknownRecord(envelope.error);
|
|
5255
|
+
const kind = telephonyEventKind(envelope);
|
|
5256
|
+
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
5257
|
+
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
5258
|
+
carrier,
|
|
5259
|
+
envelope,
|
|
5260
|
+
format: input.format,
|
|
5261
|
+
sessionId: input.sessionId
|
|
5262
|
+
}) : undefined;
|
|
5263
|
+
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
5264
|
+
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
5265
|
+
const track = firstString([media, envelope], ["track", "direction"]);
|
|
5266
|
+
return {
|
|
5267
|
+
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
5268
|
+
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
5269
|
+
carrier,
|
|
5270
|
+
direction: telephonyDirection(track),
|
|
5271
|
+
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
5272
|
+
kind,
|
|
5273
|
+
sequenceNumber,
|
|
5274
|
+
streamId
|
|
5275
|
+
};
|
|
5276
|
+
};
|
|
5277
|
+
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
5278
|
+
const envelopes = input.envelopes ?? [];
|
|
5279
|
+
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
5280
|
+
carrier: input.carrier,
|
|
5281
|
+
envelope
|
|
5282
|
+
}));
|
|
5283
|
+
const issues = [];
|
|
5284
|
+
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
5285
|
+
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
5286
|
+
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
5287
|
+
const started = startedIndex >= 0;
|
|
5288
|
+
const stopped = stoppedIndex >= 0;
|
|
5289
|
+
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
5290
|
+
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
5291
|
+
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
5292
|
+
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
5293
|
+
if ((input.requireStart ?? true) && !started) {
|
|
5294
|
+
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
5295
|
+
}
|
|
5296
|
+
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
5297
|
+
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
5298
|
+
}
|
|
5299
|
+
if ((input.requireStop ?? true) && !stopped) {
|
|
5300
|
+
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
5301
|
+
}
|
|
5302
|
+
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
5303
|
+
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
5304
|
+
}
|
|
5305
|
+
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
5306
|
+
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
5307
|
+
}
|
|
5308
|
+
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
5309
|
+
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
5310
|
+
}
|
|
5311
|
+
for (const event of events) {
|
|
5312
|
+
if (event.kind === "error") {
|
|
5313
|
+
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
return {
|
|
5317
|
+
audioBytes,
|
|
5318
|
+
carrier: input.carrier,
|
|
5319
|
+
checkedAt: Date.now(),
|
|
5320
|
+
events,
|
|
5321
|
+
issues,
|
|
5322
|
+
mediaEvents: mediaEvents.length,
|
|
5323
|
+
started,
|
|
5324
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
5325
|
+
stopped,
|
|
5326
|
+
streamIds
|
|
5327
|
+
};
|
|
5328
|
+
};
|
|
5226
5329
|
var buildMediaResamplingPlan = (input) => {
|
|
5227
5330
|
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
5228
5331
|
return {
|
package/dist/svelte/index.js
CHANGED
|
@@ -3441,6 +3441,29 @@ var telephonyDirection = (track) => {
|
|
|
3441
3441
|
return "unknown";
|
|
3442
3442
|
};
|
|
3443
3443
|
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
3444
|
+
var telephonyEventKind = (envelope) => {
|
|
3445
|
+
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
3446
|
+
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
3447
|
+
if (!normalized) {
|
|
3448
|
+
return "unknown";
|
|
3449
|
+
}
|
|
3450
|
+
if (normalized.includes("connected")) {
|
|
3451
|
+
return "connected";
|
|
3452
|
+
}
|
|
3453
|
+
if (normalized.includes("start")) {
|
|
3454
|
+
return "start";
|
|
3455
|
+
}
|
|
3456
|
+
if (normalized.includes("media")) {
|
|
3457
|
+
return "media";
|
|
3458
|
+
}
|
|
3459
|
+
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
3460
|
+
return "stop";
|
|
3461
|
+
}
|
|
3462
|
+
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
3463
|
+
return "error";
|
|
3464
|
+
}
|
|
3465
|
+
return "unknown";
|
|
3466
|
+
};
|
|
3444
3467
|
var normalizeWebRTCStat = (stat) => {
|
|
3445
3468
|
const sample = {};
|
|
3446
3469
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -3557,6 +3580,86 @@ var createTelephonyMediaSerializer = (input) => {
|
|
|
3557
3580
|
})
|
|
3558
3581
|
};
|
|
3559
3582
|
};
|
|
3583
|
+
var parseTelephonyStreamEvent = (input) => {
|
|
3584
|
+
const envelope = input.envelope;
|
|
3585
|
+
const media = unknownRecord(envelope.media);
|
|
3586
|
+
const start = unknownRecord(envelope.start);
|
|
3587
|
+
const stop = unknownRecord(envelope.stop);
|
|
3588
|
+
const errorRecord = unknownRecord(envelope.error);
|
|
3589
|
+
const kind = telephonyEventKind(envelope);
|
|
3590
|
+
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
3591
|
+
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
3592
|
+
carrier,
|
|
3593
|
+
envelope,
|
|
3594
|
+
format: input.format,
|
|
3595
|
+
sessionId: input.sessionId
|
|
3596
|
+
}) : undefined;
|
|
3597
|
+
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
3598
|
+
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
3599
|
+
const track = firstString([media, envelope], ["track", "direction"]);
|
|
3600
|
+
return {
|
|
3601
|
+
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
3602
|
+
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
3603
|
+
carrier,
|
|
3604
|
+
direction: telephonyDirection(track),
|
|
3605
|
+
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
3606
|
+
kind,
|
|
3607
|
+
sequenceNumber,
|
|
3608
|
+
streamId
|
|
3609
|
+
};
|
|
3610
|
+
};
|
|
3611
|
+
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
3612
|
+
const envelopes = input.envelopes ?? [];
|
|
3613
|
+
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
3614
|
+
carrier: input.carrier,
|
|
3615
|
+
envelope
|
|
3616
|
+
}));
|
|
3617
|
+
const issues = [];
|
|
3618
|
+
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
3619
|
+
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
3620
|
+
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
3621
|
+
const started = startedIndex >= 0;
|
|
3622
|
+
const stopped = stoppedIndex >= 0;
|
|
3623
|
+
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
3624
|
+
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
3625
|
+
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
3626
|
+
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
3627
|
+
if ((input.requireStart ?? true) && !started) {
|
|
3628
|
+
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
3629
|
+
}
|
|
3630
|
+
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
3631
|
+
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
3632
|
+
}
|
|
3633
|
+
if ((input.requireStop ?? true) && !stopped) {
|
|
3634
|
+
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
3635
|
+
}
|
|
3636
|
+
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
3637
|
+
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
3638
|
+
}
|
|
3639
|
+
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
3640
|
+
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
3641
|
+
}
|
|
3642
|
+
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
3643
|
+
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
3644
|
+
}
|
|
3645
|
+
for (const event of events) {
|
|
3646
|
+
if (event.kind === "error") {
|
|
3647
|
+
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
return {
|
|
3651
|
+
audioBytes,
|
|
3652
|
+
carrier: input.carrier,
|
|
3653
|
+
checkedAt: Date.now(),
|
|
3654
|
+
events,
|
|
3655
|
+
issues,
|
|
3656
|
+
mediaEvents: mediaEvents.length,
|
|
3657
|
+
started,
|
|
3658
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
3659
|
+
stopped,
|
|
3660
|
+
streamIds
|
|
3661
|
+
};
|
|
3662
|
+
};
|
|
3560
3663
|
var buildMediaResamplingPlan = (input) => {
|
|
3561
3664
|
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
3562
3665
|
return {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
|
-
import type { MediaFrame, MediaTelephonyCarrier, MediaTelephonyEnvelope } from '@absolutejs/media';
|
|
2
|
+
import type { MediaFrame, MediaTelephonyCarrier, MediaTelephonyEnvelope, MediaTelephonyStreamLifecycleReport } from '@absolutejs/media';
|
|
3
3
|
export type VoiceTelephonyMediaStatus = 'fail' | 'pass';
|
|
4
4
|
export type VoiceTelephonyMediaCarrierInput = {
|
|
5
5
|
carrier: MediaTelephonyCarrier;
|
|
6
6
|
envelope?: MediaTelephonyEnvelope;
|
|
7
|
+
lifecycleEnvelopes?: readonly MediaTelephonyEnvelope[];
|
|
7
8
|
};
|
|
8
9
|
export type VoiceTelephonyMediaCarrierReport = {
|
|
9
10
|
audioBytes: number;
|
|
10
11
|
carrier: MediaTelephonyCarrier;
|
|
11
12
|
frame?: MediaFrame;
|
|
12
13
|
issues: string[];
|
|
14
|
+
lifecycle: MediaTelephonyStreamLifecycleReport;
|
|
13
15
|
serialized?: MediaTelephonyEnvelope;
|
|
14
16
|
status: VoiceTelephonyMediaStatus;
|
|
15
17
|
};
|
package/dist/testing/index.js
CHANGED
|
@@ -2243,6 +2243,29 @@ var telephonyDirection = (track) => {
|
|
|
2243
2243
|
return "unknown";
|
|
2244
2244
|
};
|
|
2245
2245
|
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
2246
|
+
var telephonyEventKind = (envelope) => {
|
|
2247
|
+
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
2248
|
+
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
2249
|
+
if (!normalized) {
|
|
2250
|
+
return "unknown";
|
|
2251
|
+
}
|
|
2252
|
+
if (normalized.includes("connected")) {
|
|
2253
|
+
return "connected";
|
|
2254
|
+
}
|
|
2255
|
+
if (normalized.includes("start")) {
|
|
2256
|
+
return "start";
|
|
2257
|
+
}
|
|
2258
|
+
if (normalized.includes("media")) {
|
|
2259
|
+
return "media";
|
|
2260
|
+
}
|
|
2261
|
+
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
2262
|
+
return "stop";
|
|
2263
|
+
}
|
|
2264
|
+
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
2265
|
+
return "error";
|
|
2266
|
+
}
|
|
2267
|
+
return "unknown";
|
|
2268
|
+
};
|
|
2246
2269
|
var normalizeWebRTCStat = (stat) => {
|
|
2247
2270
|
const sample = {};
|
|
2248
2271
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -2359,6 +2382,86 @@ var createTelephonyMediaSerializer = (input) => {
|
|
|
2359
2382
|
})
|
|
2360
2383
|
};
|
|
2361
2384
|
};
|
|
2385
|
+
var parseTelephonyStreamEvent = (input) => {
|
|
2386
|
+
const envelope = input.envelope;
|
|
2387
|
+
const media = unknownRecord(envelope.media);
|
|
2388
|
+
const start = unknownRecord(envelope.start);
|
|
2389
|
+
const stop = unknownRecord(envelope.stop);
|
|
2390
|
+
const errorRecord = unknownRecord(envelope.error);
|
|
2391
|
+
const kind = telephonyEventKind(envelope);
|
|
2392
|
+
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
2393
|
+
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
2394
|
+
carrier,
|
|
2395
|
+
envelope,
|
|
2396
|
+
format: input.format,
|
|
2397
|
+
sessionId: input.sessionId
|
|
2398
|
+
}) : undefined;
|
|
2399
|
+
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
2400
|
+
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
2401
|
+
const track = firstString([media, envelope], ["track", "direction"]);
|
|
2402
|
+
return {
|
|
2403
|
+
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
2404
|
+
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
2405
|
+
carrier,
|
|
2406
|
+
direction: telephonyDirection(track),
|
|
2407
|
+
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
2408
|
+
kind,
|
|
2409
|
+
sequenceNumber,
|
|
2410
|
+
streamId
|
|
2411
|
+
};
|
|
2412
|
+
};
|
|
2413
|
+
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
2414
|
+
const envelopes = input.envelopes ?? [];
|
|
2415
|
+
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
2416
|
+
carrier: input.carrier,
|
|
2417
|
+
envelope
|
|
2418
|
+
}));
|
|
2419
|
+
const issues = [];
|
|
2420
|
+
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
2421
|
+
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
2422
|
+
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
2423
|
+
const started = startedIndex >= 0;
|
|
2424
|
+
const stopped = stoppedIndex >= 0;
|
|
2425
|
+
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
2426
|
+
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
2427
|
+
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
2428
|
+
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
2429
|
+
if ((input.requireStart ?? true) && !started) {
|
|
2430
|
+
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
2431
|
+
}
|
|
2432
|
+
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
2433
|
+
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
2434
|
+
}
|
|
2435
|
+
if ((input.requireStop ?? true) && !stopped) {
|
|
2436
|
+
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
2437
|
+
}
|
|
2438
|
+
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
2439
|
+
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
2440
|
+
}
|
|
2441
|
+
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
2442
|
+
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
2443
|
+
}
|
|
2444
|
+
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
2445
|
+
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
2446
|
+
}
|
|
2447
|
+
for (const event of events) {
|
|
2448
|
+
if (event.kind === "error") {
|
|
2449
|
+
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return {
|
|
2453
|
+
audioBytes,
|
|
2454
|
+
carrier: input.carrier,
|
|
2455
|
+
checkedAt: Date.now(),
|
|
2456
|
+
events,
|
|
2457
|
+
issues,
|
|
2458
|
+
mediaEvents: mediaEvents.length,
|
|
2459
|
+
started,
|
|
2460
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2461
|
+
stopped,
|
|
2462
|
+
streamIds
|
|
2463
|
+
};
|
|
2464
|
+
};
|
|
2362
2465
|
var buildMediaResamplingPlan = (input) => {
|
|
2363
2466
|
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
2364
2467
|
return {
|
package/dist/vue/index.js
CHANGED
|
@@ -4825,6 +4825,29 @@ var telephonyDirection = (track) => {
|
|
|
4825
4825
|
return "unknown";
|
|
4826
4826
|
};
|
|
4827
4827
|
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
4828
|
+
var telephonyEventKind = (envelope) => {
|
|
4829
|
+
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
4830
|
+
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
4831
|
+
if (!normalized) {
|
|
4832
|
+
return "unknown";
|
|
4833
|
+
}
|
|
4834
|
+
if (normalized.includes("connected")) {
|
|
4835
|
+
return "connected";
|
|
4836
|
+
}
|
|
4837
|
+
if (normalized.includes("start")) {
|
|
4838
|
+
return "start";
|
|
4839
|
+
}
|
|
4840
|
+
if (normalized.includes("media")) {
|
|
4841
|
+
return "media";
|
|
4842
|
+
}
|
|
4843
|
+
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
4844
|
+
return "stop";
|
|
4845
|
+
}
|
|
4846
|
+
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
4847
|
+
return "error";
|
|
4848
|
+
}
|
|
4849
|
+
return "unknown";
|
|
4850
|
+
};
|
|
4828
4851
|
var normalizeWebRTCStat = (stat) => {
|
|
4829
4852
|
const sample = {};
|
|
4830
4853
|
for (const [key, value] of Object.entries(stat)) {
|
|
@@ -4941,6 +4964,86 @@ var createTelephonyMediaSerializer = (input) => {
|
|
|
4941
4964
|
})
|
|
4942
4965
|
};
|
|
4943
4966
|
};
|
|
4967
|
+
var parseTelephonyStreamEvent = (input) => {
|
|
4968
|
+
const envelope = input.envelope;
|
|
4969
|
+
const media = unknownRecord(envelope.media);
|
|
4970
|
+
const start = unknownRecord(envelope.start);
|
|
4971
|
+
const stop = unknownRecord(envelope.stop);
|
|
4972
|
+
const errorRecord = unknownRecord(envelope.error);
|
|
4973
|
+
const kind = telephonyEventKind(envelope);
|
|
4974
|
+
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
4975
|
+
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
4976
|
+
carrier,
|
|
4977
|
+
envelope,
|
|
4978
|
+
format: input.format,
|
|
4979
|
+
sessionId: input.sessionId
|
|
4980
|
+
}) : undefined;
|
|
4981
|
+
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
4982
|
+
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
4983
|
+
const track = firstString([media, envelope], ["track", "direction"]);
|
|
4984
|
+
return {
|
|
4985
|
+
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
4986
|
+
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
4987
|
+
carrier,
|
|
4988
|
+
direction: telephonyDirection(track),
|
|
4989
|
+
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
4990
|
+
kind,
|
|
4991
|
+
sequenceNumber,
|
|
4992
|
+
streamId
|
|
4993
|
+
};
|
|
4994
|
+
};
|
|
4995
|
+
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
4996
|
+
const envelopes = input.envelopes ?? [];
|
|
4997
|
+
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
4998
|
+
carrier: input.carrier,
|
|
4999
|
+
envelope
|
|
5000
|
+
}));
|
|
5001
|
+
const issues = [];
|
|
5002
|
+
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
5003
|
+
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
5004
|
+
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
5005
|
+
const started = startedIndex >= 0;
|
|
5006
|
+
const stopped = stoppedIndex >= 0;
|
|
5007
|
+
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
5008
|
+
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
5009
|
+
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
5010
|
+
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
5011
|
+
if ((input.requireStart ?? true) && !started) {
|
|
5012
|
+
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
5013
|
+
}
|
|
5014
|
+
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
5015
|
+
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
5016
|
+
}
|
|
5017
|
+
if ((input.requireStop ?? true) && !stopped) {
|
|
5018
|
+
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
5019
|
+
}
|
|
5020
|
+
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
5021
|
+
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
5022
|
+
}
|
|
5023
|
+
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
5024
|
+
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
5025
|
+
}
|
|
5026
|
+
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
5027
|
+
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
5028
|
+
}
|
|
5029
|
+
for (const event of events) {
|
|
5030
|
+
if (event.kind === "error") {
|
|
5031
|
+
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
return {
|
|
5035
|
+
audioBytes,
|
|
5036
|
+
carrier: input.carrier,
|
|
5037
|
+
checkedAt: Date.now(),
|
|
5038
|
+
events,
|
|
5039
|
+
issues,
|
|
5040
|
+
mediaEvents: mediaEvents.length,
|
|
5041
|
+
started,
|
|
5042
|
+
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
5043
|
+
stopped,
|
|
5044
|
+
streamIds
|
|
5045
|
+
};
|
|
5046
|
+
};
|
|
4944
5047
|
var buildMediaResamplingPlan = (input) => {
|
|
4945
5048
|
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
4946
5049
|
return {
|
|
@@ -379,6 +379,8 @@ export declare const useVoiceReadinessFailures: (path?: string, options?: VoiceR
|
|
|
379
379
|
readonly carriers: number;
|
|
380
380
|
readonly failed: number;
|
|
381
381
|
readonly issues: number;
|
|
382
|
+
readonly lifecycleFailures: number;
|
|
383
|
+
readonly mediaEvents: number;
|
|
382
384
|
readonly passed: number;
|
|
383
385
|
readonly status: import("..").VoiceProductionReadinessStatus;
|
|
384
386
|
} | undefined;
|
|
@@ -802,6 +804,8 @@ export declare const useVoiceReadinessFailures: (path?: string, options?: VoiceR
|
|
|
802
804
|
readonly carriers: number;
|
|
803
805
|
readonly failed: number;
|
|
804
806
|
readonly issues: number;
|
|
807
|
+
readonly lifecycleFailures: number;
|
|
808
|
+
readonly mediaEvents: number;
|
|
805
809
|
readonly passed: number;
|
|
806
810
|
readonly status: import("..").VoiceProductionReadinessStatus;
|
|
807
811
|
} | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/voice",
|
|
3
|
-
"version": "0.0.22-beta.
|
|
3
|
+
"version": "0.0.22-beta.325",
|
|
4
4
|
"description": "Voice primitives and Elysia plugin for AbsoluteJS",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
}
|
|
247
247
|
},
|
|
248
248
|
"dependencies": {
|
|
249
|
-
"@absolutejs/media": "0.0.1-beta.
|
|
249
|
+
"@absolutejs/media": "0.0.1-beta.7"
|
|
250
250
|
},
|
|
251
251
|
"devDependencies": {
|
|
252
252
|
"@absolutejs/absolute": "0.19.0-beta.646",
|