@absolutejs/voice 0.0.22-beta.323 → 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.
@@ -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 {
@@ -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 and serialized back into carrier envelopes by <code>@absolutejs/media</code>.</p></section><table><thead><tr><th>Carrier</th><th>Status</th><th>Audio bytes</th><th>Frame kind</th><th>Encoding</th><th>Issues</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
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";
@@ -29776,6 +29930,12 @@ var resolveBrowserMedia = async (options, input) => {
29776
29930
  }
29777
29931
  return typeof options.browserMedia === "function" ? await options.browserMedia(input) : options.browserMedia;
29778
29932
  };
29933
+ var resolveTelephonyMedia = async (options, input) => {
29934
+ if (options.telephonyMedia === false || options.telephonyMedia === undefined) {
29935
+ return;
29936
+ }
29937
+ return typeof options.telephonyMedia === "function" ? await options.telephonyMedia(input) : options.telephonyMedia;
29938
+ };
29779
29939
  var isVoiceTelephonyWebhookSecurityReport = (value) => typeof value.generatedAt === "number" && Array.isArray(value.providers) && typeof value.status === "string";
29780
29940
  var resolveTelephonyWebhookSecurity = async (options, input) => {
29781
29941
  if (options.telephonyWebhookSecurity === false || options.telephonyWebhookSecurity === undefined) {
@@ -30161,6 +30321,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
30161
30321
  monitoringNotifierDelivery,
30162
30322
  mediaPipeline,
30163
30323
  browserMedia,
30324
+ telephonyMedia,
30164
30325
  telephonyWebhookSecurity,
30165
30326
  reconnectContracts,
30166
30327
  bargeInReports,
@@ -30205,6 +30366,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
30205
30366
  resolveMonitoringNotifierDelivery(options, { query, request }),
30206
30367
  resolveMediaPipeline(options, { query, request }),
30207
30368
  resolveBrowserMedia(options, { query, request }),
30369
+ resolveTelephonyMedia(options, { query, request }),
30208
30370
  resolveTelephonyWebhookSecurity(options, { query, request }),
30209
30371
  resolveReconnectContracts(options, { query, request }),
30210
30372
  resolveBargeInReports(options, { query, request }),
@@ -30403,6 +30565,16 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
30403
30565
  roundTripTimeMs: browserMedia.roundTripTimeMs,
30404
30566
  status: browserMedia.status === "pass" ? "pass" : "fail"
30405
30567
  } : undefined;
30568
+ const telephonyMediaSummary = telephonyMedia ? {
30569
+ audioBytes: telephonyMedia.carriers.reduce((total, carrier) => total + carrier.audioBytes, 0),
30570
+ carriers: telephonyMedia.carriers.length,
30571
+ failed: telephonyMedia.carriers.filter((carrier) => carrier.status !== "pass").length,
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),
30575
+ passed: telephonyMedia.carriers.filter((carrier) => carrier.status === "pass").length,
30576
+ status: telephonyMedia.status === "pass" ? "pass" : "fail"
30577
+ } : undefined;
30406
30578
  checks.push({
30407
30579
  detail: liveLatency.total === 0 ? "No browser live-latency measurements are recorded yet." : liveLatency.status === "pass" ? `Live browser turn latency averages ${liveLatency.averageLatencyMs}ms.` : `${liveLatency.failed} failed and ${liveLatency.warnings} warned live-latency measurement(s).`,
30408
30580
  href: firstOperationsRecordHref(operationsRecords.failingLatency) ?? options.links?.liveLatency ?? "/traces",
@@ -30495,6 +30667,31 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
30495
30667
  ]
30496
30668
  });
30497
30669
  }
30670
+ if (telephonyMedia && telephonyMediaSummary) {
30671
+ const firstIssue = telephonyMedia.issues[0];
30672
+ checks.push({
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.`,
30674
+ href: options.links?.telephonyMedia ?? "/voice/telephony-media",
30675
+ label: "Telephony media serializers",
30676
+ proofSource: proofSource("telephonyMedia", "carrierMediaSerializers"),
30677
+ gateExplanation: telephonyMediaSummary.status === "pass" ? undefined : {
30678
+ evidenceHref: options.links?.telephonyMedia ?? "/voice/telephony-media",
30679
+ observed: firstIssue ?? `${telephonyMediaSummary.issues} issue(s)`,
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",
30682
+ unit: "status"
30683
+ },
30684
+ status: telephonyMediaSummary.status,
30685
+ value: telephonyMediaSummary.status === "pass" ? `${telephonyMediaSummary.passed}/${telephonyMediaSummary.carriers}` : `${telephonyMediaSummary.failed}/${telephonyMediaSummary.carriers} failing`,
30686
+ actions: telephonyMediaSummary.status === "pass" ? [] : [
30687
+ {
30688
+ description: "Open telephony media proof and inspect carrier media lifecycle sequencing, payload parsing, MediaFrame shape, byte flow, and outbound envelope serialization.",
30689
+ href: options.links?.telephonyMedia ?? "/voice/telephony-media",
30690
+ label: "Open telephony media proof"
30691
+ }
30692
+ ]
30693
+ });
30694
+ }
30498
30695
  const carrierSummary = carriers ? {
30499
30696
  failing: carriers.summary.failing,
30500
30697
  providers: carriers.summary.providers,
@@ -31071,6 +31268,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
31071
31268
  opsActions: "/voice/ops-actions",
31072
31269
  opsRecovery: "/ops-recovery",
31073
31270
  phoneAgentSmoke: "/sessions",
31271
+ telephonyMedia: "/voice/telephony-media",
31074
31272
  telephonyWebhookSecurity: "/api/voice/telephony/webhook-security",
31075
31273
  providerContracts: "/provider-contracts",
31076
31274
  providerOrchestration: "/voice/provider-orchestration",
@@ -31124,6 +31322,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
31124
31322
  providerOrchestration: providerOrchestrationSummary,
31125
31323
  providerRecovery,
31126
31324
  phoneAgentSmokes: phoneAgentSmokeSummary,
31325
+ telephonyMedia: telephonyMediaSummary,
31127
31326
  telephonyWebhookSecurity: telephonyWebhookSecuritySummary,
31128
31327
  providerRoutingContracts: providerRoutingContractSummary,
31129
31328
  providerSlo: providerSloSummary,
@@ -20,6 +20,7 @@ import type { VoiceCampaignReadinessProofReport } from './campaign';
20
20
  import { type VoiceOpsRecoveryReport } from './opsRecovery';
21
21
  import { type VoiceObservabilityExportDeliveryHistory, type VoiceObservabilityExportDeliveryReceiptStore, type VoiceObservabilityExportReplayReport, type VoiceObservabilityExportReplaySource, type VoiceObservabilityExportReport } from './observabilityExport';
22
22
  import type { VoiceMediaPipelineReport } from './mediaPipelineRoutes';
23
+ import type { VoiceTelephonyMediaReport } from './telephonyMediaRoutes';
23
24
  import type { MediaWebRTCStatsReport } from '@absolutejs/media';
24
25
  export type VoiceProductionReadinessObservabilityExportDeliveryHistoryOptions = {
25
26
  failOnMissing?: boolean;
@@ -140,6 +141,7 @@ export type VoiceProductionReadinessReport = {
140
141
  opsRecovery?: string;
141
142
  phoneAgentSmoke?: string;
142
143
  telephonyWebhookSecurity?: string;
144
+ telephonyMedia?: string;
143
145
  providerContracts?: string;
144
146
  providerOrchestration?: string;
145
147
  providerRoutingContracts?: string;
@@ -294,6 +296,16 @@ export type VoiceProductionReadinessReport = {
294
296
  status: VoiceProductionReadinessStatus;
295
297
  warned: number;
296
298
  };
299
+ telephonyMedia?: {
300
+ audioBytes: number;
301
+ carriers: number;
302
+ failed: number;
303
+ issues: number;
304
+ lifecycleFailures: number;
305
+ mediaEvents: number;
306
+ passed: number;
307
+ status: VoiceProductionReadinessStatus;
308
+ };
297
309
  providerRoutingContracts?: {
298
310
  failed: number;
299
311
  passed: number;
@@ -481,6 +493,10 @@ export type VoiceProductionReadinessRoutesOptions = {
481
493
  query: Record<string, unknown>;
482
494
  request: Request;
483
495
  }) => Promise<MediaWebRTCStatsReport> | MediaWebRTCStatsReport);
496
+ telephonyMedia?: false | VoiceTelephonyMediaReport | ((input: {
497
+ query: Record<string, unknown>;
498
+ request: Request;
499
+ }) => Promise<VoiceTelephonyMediaReport> | VoiceTelephonyMediaReport);
484
500
  opsActionHistory?: false | VoiceProductionReadinessOpsActionHistoryOptions;
485
501
  opsRecovery?: false | VoiceOpsRecoveryReport | ((input: {
486
502
  query: Record<string, unknown>;
@@ -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 {
@@ -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
  };
@@ -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 {
@@ -57,6 +57,7 @@ export declare const useVoiceReadinessFailures: (path?: string, options?: VoiceR
57
57
  readonly opsRecovery?: string | undefined;
58
58
  readonly phoneAgentSmoke?: string | undefined;
59
59
  readonly telephonyWebhookSecurity?: string | undefined;
60
+ readonly telephonyMedia?: string | undefined;
60
61
  readonly providerContracts?: string | undefined;
61
62
  readonly providerOrchestration?: string | undefined;
62
63
  readonly providerRoutingContracts?: string | undefined;
@@ -373,6 +374,16 @@ export declare const useVoiceReadinessFailures: (path?: string, options?: VoiceR
373
374
  readonly status: import("..").VoiceProductionReadinessStatus;
374
375
  readonly warned: number;
375
376
  } | undefined;
377
+ readonly telephonyMedia?: {
378
+ readonly audioBytes: number;
379
+ readonly carriers: number;
380
+ readonly failed: number;
381
+ readonly issues: number;
382
+ readonly lifecycleFailures: number;
383
+ readonly mediaEvents: number;
384
+ readonly passed: number;
385
+ readonly status: import("..").VoiceProductionReadinessStatus;
386
+ } | undefined;
376
387
  readonly providerRoutingContracts?: {
377
388
  readonly failed: number;
378
389
  readonly passed: number;
@@ -471,6 +482,7 @@ export declare const useVoiceReadinessFailures: (path?: string, options?: VoiceR
471
482
  readonly opsRecovery?: string | undefined;
472
483
  readonly phoneAgentSmoke?: string | undefined;
473
484
  readonly telephonyWebhookSecurity?: string | undefined;
485
+ readonly telephonyMedia?: string | undefined;
474
486
  readonly providerContracts?: string | undefined;
475
487
  readonly providerOrchestration?: string | undefined;
476
488
  readonly providerRoutingContracts?: string | undefined;
@@ -787,6 +799,16 @@ export declare const useVoiceReadinessFailures: (path?: string, options?: VoiceR
787
799
  readonly status: import("..").VoiceProductionReadinessStatus;
788
800
  readonly warned: number;
789
801
  } | undefined;
802
+ readonly telephonyMedia?: {
803
+ readonly audioBytes: number;
804
+ readonly carriers: number;
805
+ readonly failed: number;
806
+ readonly issues: number;
807
+ readonly lifecycleFailures: number;
808
+ readonly mediaEvents: number;
809
+ readonly passed: number;
810
+ readonly status: import("..").VoiceProductionReadinessStatus;
811
+ } | undefined;
790
812
  readonly providerRoutingContracts?: {
791
813
  readonly failed: number;
792
814
  readonly passed: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.323",
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.6"
249
+ "@absolutejs/media": "0.0.1-beta.7"
250
250
  },
251
251
  "devDependencies": {
252
252
  "@absolutejs/absolute": "0.19.0-beta.646",