From 200716e8fecb1a3265f59680244d7bf35d8cada6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 11 Feb 2026 11:41:31 -0800 Subject: [PATCH] Add SIP transfer and SIP REFER frames to Daily transport Add write_transport_frame() hook to BaseOutputTransport so subclasses can handle custom frame types that flow through the audio queue. Add DailySIPTransferFrame and DailySIPReferFrame as DataFrame subclasses that queue with audio, ensuring SIP operations execute only after the bot finishes its current utterance. Override write_transport_frame in DailyOutputTransport to dispatch these frames to the existing sip_call_transfer() and sip_refer() client methods. Also switch DailyOutputTransport.send_message error handling from logger.error to push_error for consistency. --- src/pipecat/transports/base_output.py | 14 +++++++ src/pipecat/transports/daily/transport.py | 50 ++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/pipecat/transports/base_output.py b/src/pipecat/transports/base_output.py index c4f59a61d..6fe31aab5 100644 --- a/src/pipecat/transports/base_output.py +++ b/src/pipecat/transports/base_output.py @@ -237,6 +237,18 @@ class BaseOutputTransport(FrameProcessor): else: await self._write_dtmf_audio(frame) + async def write_transport_frame(self, frame: Frame): + """Handle a queued frame after preceding audio has been sent. + + Override in transport subclasses to handle custom frame types that + flow through the audio queue. Called by the media sender after the + frame has waited for any preceding audio to finish. + + Args: + frame: The frame to handle. + """ + pass + def _supports_native_dtmf(self) -> bool: """Override in transport implementations that support native DTMF. @@ -681,6 +693,8 @@ class BaseOutputTransport(FrameProcessor): await self._transport.send_message(frame) elif isinstance(frame, OutputDTMFFrame): await self._transport.write_dtmf(frame) + else: + await self._transport.write_transport_frame(frame) def _next_frame(self) -> AsyncGenerator[Frame, None]: """Generate the next frame for audio processing. diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index e3b246253..fd3f4e30b 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -15,7 +15,7 @@ import asyncio import time from concurrent.futures import CancelledError as FuturesCancelledError from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Awaitable, Callable, Dict, Mapping, Optional, Tuple import aiohttp @@ -26,6 +26,7 @@ from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams from pipecat.frames.frames import ( CancelFrame, ControlFrame, + DataFrame, EndFrame, Frame, InputAudioRawFrame, @@ -182,6 +183,36 @@ class DailyInputTransportMessageUrgentFrame(DailyInputTransportMessageFrame): ) +@dataclass +class DailySIPTransferFrame(DataFrame): + """SIP call transfer frame for transport queuing. + + A SIP call transfer that will be queued. The transfer will happen after any + preceding audio finishes playing, allowing the bot to complete its current + utterance before the transfer occurs. + + Parameters: + settings: SIP call transfer settings. + """ + + settings: Mapping[str, Any] = field(default_factory=dict) + + +@dataclass +class DailySIPReferFrame(DataFrame): + """SIP REFER frame for transport queuing. + + A SIP REFER that will be queued. The REFER will happen after any preceding + audio finishes playing, allowing the bot to complete its current utterance + before the REFER occurs. + + Parameters: + settings: SIP REFER settings. + """ + + settings: Mapping[str, Any] = field(default_factory=dict) + + @dataclass class DailyUpdateRemoteParticipantsFrame(ControlFrame): """Frame to update remote participants in Daily calls. @@ -1968,7 +1999,7 @@ class DailyOutputTransport(BaseOutputTransport): """ error = await self._client.send_message(frame) if error: - logger.error(f"Unable to send message: {error}") + await self.push_error(f"{self}: Unable to send message: {error}") async def register_video_destination(self, destination: str): """Register a video output destination. @@ -2011,6 +2042,21 @@ class DailyOutputTransport(BaseOutputTransport): """ return await self._client.write_video_frame(frame) + async def write_transport_frame(self, frame: Frame): + """Handle queued SIP frames after preceding audio has been sent. + + Args: + frame: The frame to handle. + """ + if isinstance(frame, DailySIPTransferFrame): + error = await self._client.sip_call_transfer(frame.settings) + if error: + await self.push_error(f"{self}: Unable to transfer SIP call: {error}") + elif isinstance(frame, DailySIPReferFrame): + error = await self._client.sip_refer(frame.settings) + if error: + await self.push_error(f"{self}: Unable to perform SIP REFER: {error}") + def _supports_native_dtmf(self) -> bool: """Daily supports native DTMF via telephone events.