@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.
@@ -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";
@@ -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) parsed into MediaFrame objects.` : firstIssue ?? `${telephonyMediaSummary.issues} telephony media serializer issue(s) need review.`,
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 serializer proof, fix payload parsing or outbound envelope serialization, then rerun readiness proof.",
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
  }
@@ -301,6 +301,8 @@ export type VoiceProductionReadinessReport = {
301
301
  carriers: number;
302
302
  failed: number;
303
303
  issues: number;
304
+ lifecycleFailures: number;
305
+ mediaEvents: number;
304
306
  passed: number;
305
307
  status: VoiceProductionReadinessStatus;
306
308
  };
@@ -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 {
@@ -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.324",
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",