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 1/4] 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. From 7501ba2e45f2c4d9521b621ef5390c7819da5e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 11 Feb 2026 11:58:14 -0800 Subject: [PATCH 2/4] Undeprecate DailyUpdateRemoteParticipantsFrame Remove the deprecation warning and __post_init__ override. Also fix the default value for remote_participants to use field(default_factory=dict) instead of None. --- changelog/3719.changed.md | 1 + src/pipecat/transports/daily/transport.py | 47 +++++------------------ 2 files changed, 10 insertions(+), 38 deletions(-) create mode 100644 changelog/3719.changed.md diff --git a/changelog/3719.changed.md b/changelog/3719.changed.md new file mode 100644 index 000000000..f42d0303b --- /dev/null +++ b/changelog/3719.changed.md @@ -0,0 +1 @@ +- `DailyUpdateRemoteParticipantsFrame` is no longer deprecated and is now queued with audio like other transport frames. diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index fd3f4e30b..5df7bcffd 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -25,7 +25,6 @@ from pydantic import BaseModel from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams from pipecat.frames.frames import ( CancelFrame, - ControlFrame, DataFrame, EndFrame, Frame, @@ -214,34 +213,14 @@ class DailySIPReferFrame(DataFrame): @dataclass -class DailyUpdateRemoteParticipantsFrame(ControlFrame): +class DailyUpdateRemoteParticipantsFrame(DataFrame): """Frame to update remote participants in Daily calls. - .. deprecated:: 0.0.87 - `DailyUpdateRemoteParticipantsFrame` is deprecated and will be removed in a future version. - Create your own custom frame and use a custom processor to handle it or use, for example, - `on_after_push_frame` event instead in the output transport. - Parameters: remote_participants: See https://reference-python.daily.co/api_reference.html#daily.CallClient.update_remote_participants. """ - remote_participants: Mapping[str, Any] = None - - def __post_init__(self): - super().__post_init__() - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn( - "DailyUpdateRemoteParticipantsFrame is deprecated and will be removed in a future version." - "Instead, create your own custom frame and handle it in the " - '`@transport.output().event_handler("on_after_push_frame")` event handler or a ' - "custom processor.", - DeprecationWarning, - stacklevel=2, - ) + remote_participants: Mapping[str, Any] = field(default_factory=dict) class WebRTCVADAnalyzer(VADAnalyzer): @@ -1977,18 +1956,6 @@ class DailyOutputTransport(BaseOutputTransport): # Leave the room. await self._client.leave() - async def process_frame(self, frame: Frame, direction: FrameDirection): - """Process outgoing frames, including transport messages. - - Args: - frame: The frame to process. - direction: The direction of frame flow in the pipeline. - """ - await super().process_frame(frame, direction) - - if isinstance(frame, DailyUpdateRemoteParticipantsFrame): - await self._client.update_remote_participants(frame.remote_participants) - async def send_message( self, frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame ): @@ -1999,7 +1966,7 @@ class DailyOutputTransport(BaseOutputTransport): """ error = await self._client.send_message(frame) if error: - await self.push_error(f"{self}: Unable to send message: {error}") + await self.push_error(f"Unable to send message: {error}") async def register_video_destination(self, destination: str): """Register a video output destination. @@ -2051,11 +2018,15 @@ class DailyOutputTransport(BaseOutputTransport): 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}") + await self.push_error(f"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}") + await self.push_error(f"Unable to perform SIP REFER: {error}") + elif isinstance(frame, DailyUpdateRemoteParticipantsFrame): + error = await self._client.update_remote_participants(frame.remote_participants) + if error: + await self.push_error(f"Unable to update remote participants: {error}") def _supports_native_dtmf(self) -> bool: """Daily supports native DTMF via telephone events. From baa61468a1848e4023b027b7fbbd277466a7876e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 11 Feb 2026 12:09:12 -0800 Subject: [PATCH 3/4] Add changelog entries for PR #3719 --- changelog/3719.added.2.md | 1 + changelog/3719.added.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/3719.added.2.md create mode 100644 changelog/3719.added.md diff --git a/changelog/3719.added.2.md b/changelog/3719.added.2.md new file mode 100644 index 000000000..77d8956d7 --- /dev/null +++ b/changelog/3719.added.2.md @@ -0,0 +1 @@ +- Added `write_transport_frame()` hook to `BaseOutputTransport` allowing transport subclasses to handle custom frame types that flow through the audio queue. diff --git a/changelog/3719.added.md b/changelog/3719.added.md new file mode 100644 index 000000000..bc1c2d6b1 --- /dev/null +++ b/changelog/3719.added.md @@ -0,0 +1 @@ +- Added `DailySIPTransferFrame` and `DailySIPReferFrame` to the Daily transport. These frames queue SIP transfer and SIP REFER operations with audio, so the operation executes only after the bot finishes its current utterance. From 352361bdd2d6abfb85b875883603f2152f3411b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 11 Feb 2026 12:09:07 -0800 Subject: [PATCH 4/4] Update changelog skill to avoid line wrapping --- .claude/skills/changelog/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/changelog/SKILL.md b/.claude/skills/changelog/SKILL.md index 89e13a40e..1ef8f324e 100644 --- a/.claude/skills/changelog/SKILL.md +++ b/.claude/skills/changelog/SKILL.md @@ -26,7 +26,7 @@ Create changelog files for the important commits in this PR. The PR number is pr - `{PR_NUMBER}.performance.md` - for performance improvements - `{PR_NUMBER}.other.md` - for other changes -4. Each changelog file should at least contain a main single line starting with `- ` followed by a clear description of the change. +4. Each changelog file should at least contain a main single line starting with `- ` followed by a clear description of the change. No line wrapping. 5. If the change is complicated, changelog files can have indented lines after the main line with additional details or code samples.