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.
This commit is contained in:
Aleix Conchillo Flaqué
2026-02-11 11:41:31 -08:00
parent 43869a499d
commit 200716e8fe
2 changed files with 62 additions and 2 deletions

View File

@@ -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.

View File

@@ -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.