


























@@ -208,6 +208,23 @@ export class TelnyxProvider implements VoiceCallProvider {
208208digits: data.payload?.digit || "",
209209};
210210211+case "call.streaming.started":
212+case "call.streaming.stopped":
213+// Informational. The realtime bridge tracks its own lifecycle via the
214+// WebSocket; we acknowledge the webhook (200) but skip event emission
215+// to avoid duplicate signal at the manager.
216+return null;
217+218+case "call.streaming.failed": {
219+const reason = typeof data.payload?.reason === "string" ? data.payload.reason : undefined;
220+return {
221+ ...baseEvent,
222+type: "call.error",
223+error: `Telnyx streaming failed${reason ? `: ${reason}` : ""}`,
224+retryable: false,
225+};
226+}
227+211228default:
212229return null;
213230}
@@ -251,18 +268,24 @@ export class TelnyxProvider implements VoiceCallProvider {
251268}
252269253270/**
254- * Initiate an outbound call via Telnyx API.
271+ * Initiate an outbound call via Telnyx API. When `input.streamUrl` is set,
272+ * the dial payload also opens a bidirectional Media Streaming session on
273+ * answer (PCMU 8 kHz), per the Telnyx documented "AI agent" pattern.
255274 */
256275async initiateCall(input: InitiateCallInput): Promise<InitiateCallResult> {
257-const result = await this.apiRequest<TelnyxCallResponse>("/calls", {
276+const body: Record<string, unknown> = {
258277connection_id: this.connectionId,
259278to: input.to,
260279from: input.from,
261280webhook_url: input.webhookUrl,
262281webhook_url_method: "POST",
263282client_state: Buffer.from(input.callId).toString("base64"),
264283timeout_secs: 30,
265-});
284+ ...(input.streamUrl
285+ ? buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken)
286+ : {}),
287+};
288+const result = await this.apiRequest<TelnyxCallResponse>("/calls", body);
266289267290return {
268291providerCallId: result.data.call_control_id,
@@ -282,12 +305,18 @@ export class TelnyxProvider implements VoiceCallProvider {
282305}
283306284307/**
285- * Answer an inbound Telnyx Call Control leg.
308+ * Answer an inbound Telnyx Call Control leg. When `input.streamUrl` is set,
309+ * the answer action also attaches a bidirectional Media Streaming session
310+ * (PCMU 8 kHz), per the Telnyx canonical "answer-action inline" pattern.
286311 */
287312async answerCall(input: AnswerCallInput): Promise<void> {
288-await this.apiRequest(`/calls/${input.providerCallId}/actions/answer`, {
313+const body: Record<string, unknown> = {
289314command_id: `openclaw-answer-${input.callId}`,
290-});
315+ ...(input.streamUrl
316+ ? buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken)
317+ : {}),
318+};
319+await this.apiRequest(`/calls/${input.providerCallId}/actions/answer`, body);
291320}
292321293322/**
@@ -355,6 +384,28 @@ export class TelnyxProvider implements VoiceCallProvider {
355384}
356385}
357386387+/**
388+ * Build the streaming-related fields for a Telnyx dial or answer-action
389+ * payload. PCMU 8 kHz mono only; bidirectional via RTP; target legs `"self"`
390+ * so the bot receives both inbound and outbound audio without the routing
391+ * gotcha that drops the call leg when `"opposite"` is configured.
392+ */
393+function buildTelnyxStreamingFields(
394+streamUrl: string,
395+streamAuthToken: string | undefined,
396+): Record<string, unknown> {
397+return {
398+stream_url: streamUrl,
399+stream_track: "inbound_track",
400+stream_codec: "PCMU",
401+stream_bidirectional_mode: "rtp",
402+stream_bidirectional_codec: "PCMU",
403+stream_bidirectional_sampling_rate: 8000,
404+stream_bidirectional_target_legs: "self",
405+ ...(streamAuthToken ? { stream_auth_token: streamAuthToken } : {}),
406+};
407+}
408+358409// -----------------------------------------------------------------------------
359410// Telnyx-specific types
360411// -----------------------------------------------------------------------------
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。