diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b4b55c76..1aaf58130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add the ability to send DTMF to `LiveKitTransport`. + - Expanded support for universal `LLMContext` to the Anthropic LLM service. Using the universal `LLMContext` and associated `LLMContextAggregatorPair` is a pre-requisite for using `LLMSwitcher` to switch between LLMs at runtime. diff --git a/src/pipecat/transports/livekit/transport.py b/src/pipecat/transports/livekit/transport.py index f21775cf6..f55fce63b 100644 --- a/src/pipecat/transports/livekit/transport.py +++ b/src/pipecat/transports/livekit/transport.py @@ -25,6 +25,8 @@ from pipecat.frames.frames import ( CancelFrame, EndFrame, OutputAudioRawFrame, + OutputDTMFFrame, + OutputDTMFUrgentFrame, StartFrame, TransportMessageFrame, TransportMessageUrgentFrame, @@ -44,6 +46,22 @@ except ModuleNotFoundError as e: logger.error("In order to use LiveKit, you need to `pip install pipecat-ai[livekit]`.") raise Exception(f"Missing module: {e}") +# DTMF mapping according to RFC 4733 +DTMF_CODE_MAP = { + "0": 0, + "1": 1, + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7, + "8": 8, + "9": 9, + "*": 10, + "#": 11, +} + @dataclass class LiveKitTransportMessageFrame(TransportMessageFrame): @@ -278,6 +296,26 @@ class LiveKitTransportClient: except Exception as e: logger.error(f"Error sending data: {e}") + async def send_dtmf(self, digit: str): + """Send DTMF tone to the room. + + Args: + digit: The DTMF digit to send (0-9, *, #). + """ + if not self._connected: + return + + if digit not in DTMF_CODE_MAP: + logger.warning(f"Invalid DTMF digit: {digit}") + return + + code = DTMF_CODE_MAP[digit] + + try: + await self.room.local_participant.publish_dtmf(code=code, digit=digit) + except Exception as e: + logger.error(f"Error sending DTMF tone {digit}: {e}") + async def publish_audio(self, audio_frame: rtc.AudioFrame): """Publish an audio frame to the room. @@ -734,6 +772,22 @@ class LiveKitOutputTransport(BaseOutputTransport): livekit_audio = self._convert_pipecat_audio_to_livekit(frame.audio) await self._client.publish_audio(livekit_audio) + def _supports_native_dtmf(self) -> bool: + """LiveKit supports native DTMF via telephone events. + + Returns: + True, as LiveKit supports native DTMF transmission. + """ + return True + + async def _write_dtmf_native(self, frame: OutputDTMFFrame | OutputDTMFUrgentFrame): + """Use LiveKit's native publish_dtmf method for telephone events. + + Args: + frame: The DTMF frame to write. + """ + await self._client.send_dtmf(frame.button.value) + def _convert_pipecat_audio_to_livekit(self, pipecat_audio: bytes) -> rtc.AudioFrame: """Convert Pipecat audio data to LiveKit audio frame.""" bytes_per_sample = 2 # Assuming 16-bit audio