

























@@ -42,6 +42,16 @@ function tryParseJson(rawMessage: string): Record<string, unknown> | null {
4242return null;
4343}
444445+function readRecordField(
46+record: Record<string, unknown>,
47+field: string,
48+): Record<string, unknown> | undefined {
49+const value = record[field];
50+return typeof value === "object" && value !== null
51+ ? (value as Record<string, unknown>)
52+ : undefined;
53+}
54+4555function normalizeBase64ForCompare(value: string): string {
4656return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
4757}
@@ -51,169 +61,170 @@ function isValidBase64Payload(value: string): boolean {
5161return normalizeBase64ForCompare(buffer.toString("base64")) === normalizeBase64ForCompare(value);
5262}
536364+function parseMediaFrame(msg: Record<string, unknown>): StreamFrame {
65+const mediaData = readRecordField(msg, "media");
66+const payload = typeof mediaData?.payload === "string" ? mediaData.payload : undefined;
67+if (!payload || !isValidBase64Payload(payload)) {
68+return { kind: "ignored" };
69+}
70+return {
71+kind: "media",
72+payloadBase64: payload,
73+timestampMs: parseTimestampMs(mediaData?.timestamp),
74+track: typeof mediaData?.track === "string" ? mediaData.track : undefined,
75+};
76+}
77+78+function parseMarkFrame(msg: Record<string, unknown>): StreamFrame {
79+const markData = readRecordField(msg, "mark");
80+const name = typeof markData?.name === "string" ? markData.name : undefined;
81+return { kind: "mark", name };
82+}
83+84+type ProviderStartFrameParser = (msg: Record<string, unknown>) => StreamFrame | undefined;
85+type ProviderExtraFrameParser = (
86+event: unknown,
87+msg: Record<string, unknown>,
88+) => StreamFrame | undefined;
89+90+function parseCommonInboundFrame(
91+event: unknown,
92+msg: Record<string, unknown>,
93+): StreamFrame | undefined {
94+if (event === "media") {
95+return parseMediaFrame(msg);
96+}
97+if (event === "mark") {
98+return parseMarkFrame(msg);
99+}
100+if (event === "stop") {
101+return { kind: "stop" };
102+}
103+return undefined;
104+}
105+106+function parseProviderInboundFrame(
107+rawMessage: string,
108+parseStartFrame: ProviderStartFrameParser,
109+parseExtraFrame?: ProviderExtraFrameParser,
110+): StreamFrame {
111+const msg = tryParseJson(rawMessage);
112+if (!msg) {
113+return { kind: "ignored" };
114+}
115+const event = msg.event;
116+if (event === "start") {
117+return parseStartFrame(msg) ?? { kind: "ignored" };
118+}
119+return (
120+parseCommonInboundFrame(event, msg) ?? parseExtraFrame?.(event, msg) ?? { kind: "ignored" }
121+);
122+}
123+124+function withOptionalStreamSid(streamSid: string | undefined): Partial<{ streamSid: string }> {
125+return streamSid === undefined ? {} : { streamSid };
126+}
127+128+function serializeMediaFrame(payloadBase64: string, streamSid?: string): string {
129+return JSON.stringify({
130+event: "media",
131+ ...withOptionalStreamSid(streamSid),
132+media: { payload: payloadBase64 },
133+});
134+}
135+136+function serializeClearFrame(streamSid?: string): string {
137+return JSON.stringify({ event: "clear", ...withOptionalStreamSid(streamSid) });
138+}
139+140+function serializeMarkFrame(name: string, streamSid?: string): string {
141+return JSON.stringify({
142+event: "mark",
143+ ...withOptionalStreamSid(streamSid),
144+mark: { name },
145+});
146+}
147+54148export class TwilioStreamFrameAdapter implements StreamFrameAdapter {
55149readonly providerName = "twilio" as const;
56150private streamSid = "";
5715158152parseInbound(rawMessage: string): StreamFrame {
59-const msg = tryParseJson(rawMessage);
60-if (!msg) {
61-return { kind: "ignored" };
62-}
63-const event = msg.event;
64-if (event === "start") {
65-const startData =
66-typeof msg.start === "object" && msg.start !== null
67- ? (msg.start as Record<string, unknown>)
68- : undefined;
153+return parseProviderInboundFrame(rawMessage, (msg) => {
154+const startData = readRecordField(msg, "start");
69155const streamSid = typeof startData?.streamSid === "string" ? startData.streamSid : "";
70156const callSid = typeof startData?.callSid === "string" ? startData.callSid : "";
71157if (!streamSid || !callSid) {
72-return { kind: "ignored" };
158+return undefined;
73159}
74160this.streamSid = streamSid;
75161return { kind: "start", streamId: streamSid, providerCallId: callSid };
76-}
77-if (event === "media") {
78-const mediaData =
79-typeof msg.media === "object" && msg.media !== null
80- ? (msg.media as Record<string, unknown>)
81- : undefined;
82-const payload = typeof mediaData?.payload === "string" ? mediaData.payload : undefined;
83-if (!payload || !isValidBase64Payload(payload)) {
84-return { kind: "ignored" };
85-}
86-return {
87-kind: "media",
88-payloadBase64: payload,
89-timestampMs: parseTimestampMs(mediaData?.timestamp),
90-track: typeof mediaData?.track === "string" ? mediaData.track : undefined,
91-};
92-}
93-if (event === "mark") {
94-const markData =
95-typeof msg.mark === "object" && msg.mark !== null
96- ? (msg.mark as Record<string, unknown>)
97- : undefined;
98-const name = typeof markData?.name === "string" ? markData.name : undefined;
99-return { kind: "mark", name };
100-}
101-if (event === "stop") {
102-return { kind: "stop" };
103-}
104-return { kind: "ignored" };
162+});
105163}
106164107165serializeMedia(payloadBase64: string): string {
108-return JSON.stringify({
109-event: "media",
110-streamSid: this.streamSid,
111-media: { payload: payloadBase64 },
112-});
166+return serializeMediaFrame(payloadBase64, this.streamSid);
113167}
114168115169serializeClear(): string {
116-return JSON.stringify({ event: "clear", streamSid: this.streamSid });
170+return serializeClearFrame(this.streamSid);
117171}
118172119173serializeMark(name: string): string {
120-return JSON.stringify({
121-event: "mark",
122-streamSid: this.streamSid,
123-mark: { name },
124-});
174+return serializeMarkFrame(name, this.streamSid);
125175}
126176}
127177128178export class TelnyxStreamFrameAdapter implements StreamFrameAdapter {
129179readonly providerName = "telnyx" as const;
130180131181parseInbound(rawMessage: string): StreamFrame {
132-const msg = tryParseJson(rawMessage);
133-if (!msg) {
134-return { kind: "ignored" };
135-}
136-const event = msg.event;
137-const topLevelStreamId =
138-typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : undefined;
139-if (event === "start") {
140-const startData =
141-typeof msg.start === "object" && msg.start !== null
142- ? (msg.start as Record<string, unknown>)
143- : undefined;
144-const providerCallId =
145-typeof startData?.call_control_id === "string" && startData.call_control_id
146- ? startData.call_control_id
147- : undefined;
148-if (!topLevelStreamId || !providerCallId) {
149-return { kind: "ignored" };
150-}
151-return {
152-kind: "start",
153-streamId: topLevelStreamId,
154- providerCallId,
155-};
156-}
157-if (event === "media") {
158-const mediaData =
159-typeof msg.media === "object" && msg.media !== null
160- ? (msg.media as Record<string, unknown>)
161- : undefined;
162-const payload = typeof mediaData?.payload === "string" ? mediaData.payload : undefined;
163-if (!payload || !isValidBase64Payload(payload)) {
164-return { kind: "ignored" };
165-}
166-return {
167-kind: "media",
168-payloadBase64: payload,
169-timestampMs: parseTimestampMs(mediaData?.timestamp),
170-track: typeof mediaData?.track === "string" ? mediaData.track : undefined,
171-};
172-}
173-if (event === "mark") {
174-const markData =
175-typeof msg.mark === "object" && msg.mark !== null
176- ? (msg.mark as Record<string, unknown>)
177- : undefined;
178-const name = typeof markData?.name === "string" ? markData.name : undefined;
179-return { kind: "mark", name };
180-}
181-if (event === "stop") {
182-return { kind: "stop" };
183-}
184-if (event === "error") {
185-const errorData =
186-typeof msg.payload === "object" && msg.payload !== null
187- ? (msg.payload as Record<string, unknown>)
188- : undefined;
189-return {
190-kind: "error",
191-code:
192-typeof errorData?.code === "string" || typeof errorData?.code === "number"
193- ? String(errorData.code)
194- : undefined,
195-title: typeof errorData?.title === "string" ? errorData.title : undefined,
196-detail: typeof errorData?.detail === "string" ? errorData.detail : undefined,
197-};
198-}
199-return { kind: "ignored" };
182+return parseProviderInboundFrame(
183+rawMessage,
184+(msg) => {
185+const topLevelStreamId =
186+typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : undefined;
187+const startData = readRecordField(msg, "start");
188+const providerCallId =
189+typeof startData?.call_control_id === "string" && startData.call_control_id
190+ ? startData.call_control_id
191+ : undefined;
192+if (!topLevelStreamId || !providerCallId) {
193+return undefined;
194+}
195+return {
196+kind: "start",
197+streamId: topLevelStreamId,
198+ providerCallId,
199+};
200+},
201+(event, msg) => {
202+if (event !== "error") {
203+return undefined;
204+}
205+const errorData = readRecordField(msg, "payload");
206+return {
207+kind: "error",
208+code:
209+typeof errorData?.code === "string" || typeof errorData?.code === "number"
210+ ? String(errorData.code)
211+ : undefined,
212+title: typeof errorData?.title === "string" ? errorData.title : undefined,
213+detail: typeof errorData?.detail === "string" ? errorData.detail : undefined,
214+};
215+},
216+);
200217}
201218202219serializeMedia(payloadBase64: string): string {
203-return JSON.stringify({
204-event: "media",
205-media: { payload: payloadBase64 },
206-});
220+return serializeMediaFrame(payloadBase64);
207221}
208222209223serializeClear(): string {
210-return JSON.stringify({ event: "clear" });
224+return serializeClearFrame();
211225}
212226213227serializeMark(name: string): string {
214-return JSON.stringify({
215-event: "mark",
216-mark: { name },
217-});
228+return serializeMarkFrame(name);
218229}
219230}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。