dv-pipecat-ai 0.0.82.dev857__py3-none-any.whl → 0.0.85.dev837__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dv-pipecat-ai might be problematic. Click here for more details.
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/METADATA +98 -130
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/RECORD +192 -140
- pipecat/adapters/base_llm_adapter.py +38 -1
- pipecat/adapters/services/anthropic_adapter.py +9 -14
- pipecat/adapters/services/aws_nova_sonic_adapter.py +120 -5
- pipecat/adapters/services/bedrock_adapter.py +236 -13
- pipecat/adapters/services/gemini_adapter.py +12 -8
- pipecat/adapters/services/open_ai_adapter.py +19 -7
- pipecat/adapters/services/open_ai_realtime_adapter.py +5 -0
- pipecat/audio/dtmf/dtmf-0.wav +0 -0
- pipecat/audio/dtmf/dtmf-1.wav +0 -0
- pipecat/audio/dtmf/dtmf-2.wav +0 -0
- pipecat/audio/dtmf/dtmf-3.wav +0 -0
- pipecat/audio/dtmf/dtmf-4.wav +0 -0
- pipecat/audio/dtmf/dtmf-5.wav +0 -0
- pipecat/audio/dtmf/dtmf-6.wav +0 -0
- pipecat/audio/dtmf/dtmf-7.wav +0 -0
- pipecat/audio/dtmf/dtmf-8.wav +0 -0
- pipecat/audio/dtmf/dtmf-9.wav +0 -0
- pipecat/audio/dtmf/dtmf-pound.wav +0 -0
- pipecat/audio/dtmf/dtmf-star.wav +0 -0
- pipecat/audio/filters/krisp_viva_filter.py +193 -0
- pipecat/audio/filters/noisereduce_filter.py +15 -0
- pipecat/audio/turn/base_turn_analyzer.py +9 -1
- pipecat/audio/turn/smart_turn/base_smart_turn.py +14 -8
- pipecat/audio/turn/smart_turn/data/__init__.py +0 -0
- pipecat/audio/turn/smart_turn/data/smart-turn-v3.0.onnx +0 -0
- pipecat/audio/turn/smart_turn/http_smart_turn.py +6 -2
- pipecat/audio/turn/smart_turn/local_smart_turn.py +1 -1
- pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +1 -1
- pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +124 -0
- pipecat/audio/vad/data/README.md +10 -0
- pipecat/audio/vad/data/silero_vad_v2.onnx +0 -0
- pipecat/audio/vad/silero.py +9 -3
- pipecat/audio/vad/vad_analyzer.py +13 -1
- pipecat/extensions/voicemail/voicemail_detector.py +5 -5
- pipecat/frames/frames.py +277 -86
- pipecat/observers/loggers/debug_log_observer.py +3 -3
- pipecat/observers/loggers/llm_log_observer.py +7 -3
- pipecat/observers/loggers/user_bot_latency_log_observer.py +22 -10
- pipecat/pipeline/runner.py +18 -6
- pipecat/pipeline/service_switcher.py +64 -36
- pipecat/pipeline/task.py +125 -79
- pipecat/pipeline/tts_switcher.py +30 -0
- pipecat/processors/aggregators/dtmf_aggregator.py +2 -3
- pipecat/processors/aggregators/{gated_openai_llm_context.py → gated_llm_context.py} +9 -9
- pipecat/processors/aggregators/gated_open_ai_llm_context.py +12 -0
- pipecat/processors/aggregators/llm_context.py +40 -2
- pipecat/processors/aggregators/llm_response.py +32 -15
- pipecat/processors/aggregators/llm_response_universal.py +19 -15
- pipecat/processors/aggregators/user_response.py +6 -6
- pipecat/processors/aggregators/vision_image_frame.py +24 -2
- pipecat/processors/audio/audio_buffer_processor.py +43 -8
- pipecat/processors/dtmf_aggregator.py +174 -77
- pipecat/processors/filters/stt_mute_filter.py +17 -0
- pipecat/processors/frame_processor.py +110 -24
- pipecat/processors/frameworks/langchain.py +8 -2
- pipecat/processors/frameworks/rtvi.py +210 -68
- pipecat/processors/frameworks/strands_agents.py +170 -0
- pipecat/processors/logger.py +2 -2
- pipecat/processors/transcript_processor.py +26 -5
- pipecat/processors/user_idle_processor.py +35 -11
- pipecat/runner/daily.py +59 -20
- pipecat/runner/run.py +395 -93
- pipecat/runner/types.py +6 -4
- pipecat/runner/utils.py +51 -10
- pipecat/serializers/__init__.py +5 -1
- pipecat/serializers/asterisk.py +16 -2
- pipecat/serializers/convox.py +41 -4
- pipecat/serializers/custom.py +257 -0
- pipecat/serializers/exotel.py +5 -5
- pipecat/serializers/livekit.py +20 -0
- pipecat/serializers/plivo.py +5 -5
- pipecat/serializers/protobuf.py +6 -5
- pipecat/serializers/telnyx.py +2 -2
- pipecat/serializers/twilio.py +43 -23
- pipecat/serializers/vi.py +324 -0
- pipecat/services/ai_service.py +2 -6
- pipecat/services/anthropic/llm.py +2 -25
- pipecat/services/assemblyai/models.py +6 -0
- pipecat/services/assemblyai/stt.py +13 -5
- pipecat/services/asyncai/tts.py +5 -3
- pipecat/services/aws/__init__.py +1 -0
- pipecat/services/aws/llm.py +147 -105
- pipecat/services/aws/nova_sonic/__init__.py +0 -0
- pipecat/services/aws/nova_sonic/context.py +436 -0
- pipecat/services/aws/nova_sonic/frames.py +25 -0
- pipecat/services/aws/nova_sonic/llm.py +1265 -0
- pipecat/services/aws/stt.py +3 -3
- pipecat/services/aws_nova_sonic/__init__.py +19 -1
- pipecat/services/aws_nova_sonic/aws.py +11 -1151
- pipecat/services/aws_nova_sonic/context.py +8 -354
- pipecat/services/aws_nova_sonic/frames.py +13 -17
- pipecat/services/azure/llm.py +51 -1
- pipecat/services/azure/realtime/__init__.py +0 -0
- pipecat/services/azure/realtime/llm.py +65 -0
- pipecat/services/azure/stt.py +15 -0
- pipecat/services/cartesia/stt.py +77 -70
- pipecat/services/cartesia/tts.py +80 -13
- pipecat/services/deepgram/__init__.py +1 -0
- pipecat/services/deepgram/flux/__init__.py +0 -0
- pipecat/services/deepgram/flux/stt.py +640 -0
- pipecat/services/elevenlabs/__init__.py +4 -1
- pipecat/services/elevenlabs/stt.py +339 -0
- pipecat/services/elevenlabs/tts.py +87 -46
- pipecat/services/fish/tts.py +5 -2
- pipecat/services/gemini_multimodal_live/events.py +38 -524
- pipecat/services/gemini_multimodal_live/file_api.py +23 -173
- pipecat/services/gemini_multimodal_live/gemini.py +41 -1403
- pipecat/services/gladia/stt.py +56 -72
- pipecat/services/google/__init__.py +1 -0
- pipecat/services/google/gemini_live/__init__.py +3 -0
- pipecat/services/google/gemini_live/file_api.py +189 -0
- pipecat/services/google/gemini_live/llm.py +1582 -0
- pipecat/services/google/gemini_live/llm_vertex.py +184 -0
- pipecat/services/google/llm.py +15 -11
- pipecat/services/google/llm_openai.py +3 -3
- pipecat/services/google/llm_vertex.py +86 -16
- pipecat/services/google/stt.py +4 -0
- pipecat/services/google/tts.py +7 -3
- pipecat/services/heygen/api.py +2 -0
- pipecat/services/heygen/client.py +8 -4
- pipecat/services/heygen/video.py +2 -0
- pipecat/services/hume/__init__.py +5 -0
- pipecat/services/hume/tts.py +220 -0
- pipecat/services/inworld/tts.py +6 -6
- pipecat/services/llm_service.py +15 -5
- pipecat/services/lmnt/tts.py +4 -2
- pipecat/services/mcp_service.py +4 -2
- pipecat/services/mem0/memory.py +6 -5
- pipecat/services/mistral/llm.py +29 -8
- pipecat/services/moondream/vision.py +42 -16
- pipecat/services/neuphonic/tts.py +5 -2
- pipecat/services/openai/__init__.py +1 -0
- pipecat/services/openai/base_llm.py +27 -20
- pipecat/services/openai/realtime/__init__.py +0 -0
- pipecat/services/openai/realtime/context.py +272 -0
- pipecat/services/openai/realtime/events.py +1106 -0
- pipecat/services/openai/realtime/frames.py +37 -0
- pipecat/services/openai/realtime/llm.py +829 -0
- pipecat/services/openai/tts.py +49 -10
- pipecat/services/openai_realtime/__init__.py +27 -0
- pipecat/services/openai_realtime/azure.py +21 -0
- pipecat/services/openai_realtime/context.py +21 -0
- pipecat/services/openai_realtime/events.py +21 -0
- pipecat/services/openai_realtime/frames.py +21 -0
- pipecat/services/openai_realtime_beta/azure.py +16 -0
- pipecat/services/openai_realtime_beta/openai.py +17 -5
- pipecat/services/piper/tts.py +7 -9
- pipecat/services/playht/tts.py +34 -4
- pipecat/services/rime/tts.py +12 -12
- pipecat/services/riva/stt.py +3 -1
- pipecat/services/salesforce/__init__.py +9 -0
- pipecat/services/salesforce/llm.py +700 -0
- pipecat/services/sarvam/__init__.py +7 -0
- pipecat/services/sarvam/stt.py +540 -0
- pipecat/services/sarvam/tts.py +97 -13
- pipecat/services/simli/video.py +2 -2
- pipecat/services/speechmatics/stt.py +22 -10
- pipecat/services/stt_service.py +47 -0
- pipecat/services/tavus/video.py +2 -2
- pipecat/services/tts_service.py +75 -22
- pipecat/services/vision_service.py +7 -6
- pipecat/services/vistaar/llm.py +51 -9
- pipecat/tests/utils.py +4 -4
- pipecat/transcriptions/language.py +41 -1
- pipecat/transports/base_input.py +13 -34
- pipecat/transports/base_output.py +140 -104
- pipecat/transports/daily/transport.py +199 -26
- pipecat/transports/heygen/__init__.py +0 -0
- pipecat/transports/heygen/transport.py +381 -0
- pipecat/transports/livekit/transport.py +228 -63
- pipecat/transports/local/audio.py +6 -1
- pipecat/transports/local/tk.py +11 -2
- pipecat/transports/network/fastapi_websocket.py +1 -1
- pipecat/transports/smallwebrtc/connection.py +103 -19
- pipecat/transports/smallwebrtc/request_handler.py +246 -0
- pipecat/transports/smallwebrtc/transport.py +65 -23
- pipecat/transports/tavus/transport.py +23 -12
- pipecat/transports/websocket/client.py +41 -5
- pipecat/transports/websocket/fastapi.py +21 -11
- pipecat/transports/websocket/server.py +14 -7
- pipecat/transports/whatsapp/api.py +8 -0
- pipecat/transports/whatsapp/client.py +47 -0
- pipecat/utils/base_object.py +54 -22
- pipecat/utils/redis.py +58 -0
- pipecat/utils/string.py +13 -1
- pipecat/utils/tracing/service_decorators.py +21 -21
- pipecat/serializers/genesys.py +0 -95
- pipecat/services/google/test-google-chirp.py +0 -45
- pipecat/services/openai.py +0 -698
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.82.dev857.dist-info → dv_pipecat_ai-0.0.85.dev837.dist-info}/top_level.txt +0 -0
- /pipecat/services/{aws_nova_sonic → aws/nova_sonic}/ready.wav +0 -0
|
@@ -95,15 +95,20 @@ class SmallWebRTCTrack:
|
|
|
95
95
|
enable/disable control and frame discarding for audio and video streams.
|
|
96
96
|
"""
|
|
97
97
|
|
|
98
|
-
def __init__(self,
|
|
98
|
+
def __init__(self, receiver):
|
|
99
99
|
"""Initialize the WebRTC track wrapper.
|
|
100
100
|
|
|
101
101
|
Args:
|
|
102
|
-
|
|
103
|
-
index: The index of the track in the transceiver (0 for mic, 1 for cam, 2 for screen)
|
|
102
|
+
receiver: The RemoteStreamTrack receiver instance.
|
|
104
103
|
"""
|
|
105
|
-
self.
|
|
104
|
+
self._receiver = receiver
|
|
105
|
+
# Configuring the receiver for not consuming the track by default to prevent memory grow
|
|
106
|
+
self._receiver._enabled = False
|
|
107
|
+
self._track = receiver.track
|
|
106
108
|
self._enabled = True
|
|
109
|
+
self._last_recv_time: float = 0.0
|
|
110
|
+
self._idle_task: Optional[asyncio.Task] = None
|
|
111
|
+
self._idle_timeout: float = 2.0 # seconds before discarding old frames
|
|
107
112
|
|
|
108
113
|
def set_enabled(self, enabled: bool) -> None:
|
|
109
114
|
"""Enable or disable the track.
|
|
@@ -138,13 +143,44 @@ class SmallWebRTCTrack:
|
|
|
138
143
|
async def recv(self) -> Optional[Frame]:
|
|
139
144
|
"""Receive the next frame from the track.
|
|
140
145
|
|
|
146
|
+
Enables the internal receiving state and starts idle watcher.
|
|
147
|
+
|
|
141
148
|
Returns:
|
|
142
149
|
The next frame, except for video tracks, where it returns the frame only if the track is enabled, otherwise, returns None.
|
|
143
150
|
"""
|
|
151
|
+
self._receiver._enabled = True
|
|
152
|
+
self._last_recv_time = time.time()
|
|
153
|
+
|
|
154
|
+
# start idle watcher if not already running
|
|
155
|
+
if not self._idle_task or self._idle_task.done():
|
|
156
|
+
self._idle_task = asyncio.create_task(self._idle_watcher())
|
|
157
|
+
|
|
144
158
|
if not self._enabled and self._track.kind == "video":
|
|
145
159
|
return None
|
|
146
160
|
return await self._track.recv()
|
|
147
161
|
|
|
162
|
+
async def _idle_watcher(self):
|
|
163
|
+
"""Disable receiving if idle for more than _idle_timeout and monitor queue size."""
|
|
164
|
+
while self._receiver._enabled:
|
|
165
|
+
await asyncio.sleep(self._idle_timeout)
|
|
166
|
+
idle_duration = time.time() - self._last_recv_time
|
|
167
|
+
if idle_duration >= self._idle_timeout:
|
|
168
|
+
# discard old frames to prevent memory growth
|
|
169
|
+
logger.debug(
|
|
170
|
+
f"Disabling receiver for {self._track.kind} track after {idle_duration:.2f}s idle"
|
|
171
|
+
)
|
|
172
|
+
await self.discard_old_frames()
|
|
173
|
+
self._receiver._enabled = False
|
|
174
|
+
|
|
175
|
+
def stop(self):
|
|
176
|
+
"""Stop receiving frames from the track."""
|
|
177
|
+
self._receiver._enabled = False
|
|
178
|
+
if self._idle_task:
|
|
179
|
+
self._idle_task.cancel()
|
|
180
|
+
self._idle_task = None
|
|
181
|
+
if self._track:
|
|
182
|
+
self._track.stop()
|
|
183
|
+
|
|
148
184
|
def __getattr__(self, name):
|
|
149
185
|
"""Forward attribute access to the underlying track.
|
|
150
186
|
|
|
@@ -170,11 +206,16 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
170
206
|
for real-time audio/video communication.
|
|
171
207
|
"""
|
|
172
208
|
|
|
173
|
-
def __init__(
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
ice_servers: Optional[Union[List[str], List[IceServer]]] = None,
|
|
212
|
+
connection_timeout_secs: int = 60,
|
|
213
|
+
):
|
|
174
214
|
"""Initialize the WebRTC connection.
|
|
175
215
|
|
|
176
216
|
Args:
|
|
177
217
|
ice_servers: List of ICE servers as URLs or IceServer objects.
|
|
218
|
+
connection_timeout_secs: Timeout in seconds for connecting to the peer.
|
|
178
219
|
|
|
179
220
|
Raises:
|
|
180
221
|
TypeError: If ice_servers contains mixed types or unsupported types.
|
|
@@ -195,6 +236,7 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
195
236
|
VIDEO_TRANSCEIVER_INDEX: self.video_input_track,
|
|
196
237
|
SCREEN_VIDEO_TRANSCEIVER_INDEX: self.screen_video_input_track,
|
|
197
238
|
}
|
|
239
|
+
self.connection_timeout_secs = connection_timeout_secs
|
|
198
240
|
|
|
199
241
|
self._initialize()
|
|
200
242
|
|
|
@@ -241,8 +283,8 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
241
283
|
self._data_channel = None
|
|
242
284
|
self._renegotiation_in_progress = False
|
|
243
285
|
self._last_received_time = None
|
|
244
|
-
self._message_queue = []
|
|
245
286
|
self._pending_app_messages = []
|
|
287
|
+
self._connecting_timeout_task = None
|
|
246
288
|
|
|
247
289
|
def _setup_listeners(self):
|
|
248
290
|
"""Set up event listeners for the peer connection."""
|
|
@@ -254,10 +296,7 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
254
296
|
# Flush queued messages once the data channel is open
|
|
255
297
|
@channel.on("open")
|
|
256
298
|
async def on_open():
|
|
257
|
-
logger.debug("Data channel is open
|
|
258
|
-
while self._message_queue:
|
|
259
|
-
message = self._message_queue.pop(0)
|
|
260
|
-
self._data_channel.send(message)
|
|
299
|
+
logger.debug("Data channel is open!")
|
|
261
300
|
|
|
262
301
|
@channel.on("message")
|
|
263
302
|
async def on_message(message):
|
|
@@ -454,11 +493,15 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
454
493
|
|
|
455
494
|
async def _close(self):
|
|
456
495
|
"""Close the peer connection and cleanup resources."""
|
|
496
|
+
for track in self._track_map.values():
|
|
497
|
+
if track:
|
|
498
|
+
track.stop()
|
|
499
|
+
self._track_map.clear()
|
|
457
500
|
if self._pc:
|
|
458
501
|
await self._pc.close()
|
|
459
|
-
self._message_queue.clear()
|
|
460
502
|
self._pending_app_messages.clear()
|
|
461
503
|
self._track_map = {}
|
|
504
|
+
self._cancel_monitoring_connecting_state()
|
|
462
505
|
|
|
463
506
|
def get_answer(self):
|
|
464
507
|
"""Get the SDP answer for the current connection.
|
|
@@ -476,9 +519,45 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
476
519
|
"pc_id": self._pc_id,
|
|
477
520
|
}
|
|
478
521
|
|
|
522
|
+
def _monitoring_connecting_state(self) -> None:
|
|
523
|
+
"""Start monitoring the peer connection while it is in the *connecting* state.
|
|
524
|
+
|
|
525
|
+
This method schedules a timeout task that will automatically close the
|
|
526
|
+
connection if it remains in the connecting state for more than the specified
|
|
527
|
+
timeout, default to 60 seconds.
|
|
528
|
+
"""
|
|
529
|
+
logger.debug("Monitoring connecting state")
|
|
530
|
+
|
|
531
|
+
async def timeout_handler():
|
|
532
|
+
# We will close the connection in case we have remained in the connecting state for over 1 minute
|
|
533
|
+
await asyncio.sleep(self.connection_timeout_secs)
|
|
534
|
+
logger.warning("Timeout establishing the connection to the remote peer. Closing.")
|
|
535
|
+
|
|
536
|
+
await self._close()
|
|
537
|
+
|
|
538
|
+
# Create and store the timeout task
|
|
539
|
+
self._connecting_timeout_task = asyncio.create_task(timeout_handler())
|
|
540
|
+
|
|
541
|
+
def _cancel_monitoring_connecting_state(self) -> None:
|
|
542
|
+
"""Cancel the ongoing connecting-state timeout task, if any.
|
|
543
|
+
|
|
544
|
+
This method should be called once the connection has either succeeded or
|
|
545
|
+
transitioned out of the connecting state. If the timeout task is still
|
|
546
|
+
pending, it will be canceled and the reference cleared.
|
|
547
|
+
"""
|
|
548
|
+
if self._connecting_timeout_task and not self._connecting_timeout_task.done():
|
|
549
|
+
logger.debug("Cancelling the connecting timeout task")
|
|
550
|
+
self._connecting_timeout_task.cancel()
|
|
551
|
+
self._connecting_timeout_task = None
|
|
552
|
+
|
|
479
553
|
async def _handle_new_connection_state(self):
|
|
480
554
|
"""Handle changes in the peer connection state."""
|
|
481
555
|
state = self._pc.connectionState
|
|
556
|
+
if state == "connecting":
|
|
557
|
+
self._monitoring_connecting_state()
|
|
558
|
+
else:
|
|
559
|
+
self._cancel_monitoring_connecting_state()
|
|
560
|
+
|
|
482
561
|
if state == "connected" and not self._connect_invoked:
|
|
483
562
|
# We are going to wait until the pipeline is ready before triggering the event
|
|
484
563
|
return
|
|
@@ -526,8 +605,8 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
526
605
|
logger.warning("No audio transceiver is available")
|
|
527
606
|
return None
|
|
528
607
|
|
|
529
|
-
|
|
530
|
-
audio_track = SmallWebRTCTrack(
|
|
608
|
+
receiver = transceivers[AUDIO_TRANSCEIVER_INDEX].receiver
|
|
609
|
+
audio_track = SmallWebRTCTrack(receiver) if receiver else None
|
|
531
610
|
self._track_map[AUDIO_TRANSCEIVER_INDEX] = audio_track
|
|
532
611
|
return audio_track
|
|
533
612
|
|
|
@@ -548,8 +627,8 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
548
627
|
logger.warning("No video transceiver is available")
|
|
549
628
|
return None
|
|
550
629
|
|
|
551
|
-
|
|
552
|
-
video_track = SmallWebRTCTrack(
|
|
630
|
+
receiver = transceivers[VIDEO_TRANSCEIVER_INDEX].receiver
|
|
631
|
+
video_track = SmallWebRTCTrack(receiver) if receiver else None
|
|
553
632
|
self._track_map[VIDEO_TRANSCEIVER_INDEX] = video_track
|
|
554
633
|
return video_track
|
|
555
634
|
|
|
@@ -570,8 +649,8 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
570
649
|
logger.warning("No screen video transceiver is available")
|
|
571
650
|
return None
|
|
572
651
|
|
|
573
|
-
|
|
574
|
-
video_track = SmallWebRTCTrack(
|
|
652
|
+
receiver = transceivers[SCREEN_VIDEO_TRANSCEIVER_INDEX].receiver
|
|
653
|
+
video_track = SmallWebRTCTrack(receiver) if receiver else None
|
|
575
654
|
self._track_map[SCREEN_VIDEO_TRANSCEIVER_INDEX] = video_track
|
|
576
655
|
return video_track
|
|
577
656
|
|
|
@@ -585,8 +664,8 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
585
664
|
if self._data_channel and self._data_channel.readyState == "open":
|
|
586
665
|
self._data_channel.send(json_message)
|
|
587
666
|
else:
|
|
588
|
-
|
|
589
|
-
|
|
667
|
+
# The client might choose never to create a data channel.
|
|
668
|
+
logger.trace("Data channel not ready, discarding message!")
|
|
590
669
|
|
|
591
670
|
def ask_to_renegotiate(self):
|
|
592
671
|
"""Request renegotiation of the WebRTC connection."""
|
|
@@ -610,3 +689,8 @@ class SmallWebRTCConnection(BaseObject):
|
|
|
610
689
|
)()
|
|
611
690
|
if track:
|
|
612
691
|
track.set_enabled(signalling_message.enabled)
|
|
692
|
+
|
|
693
|
+
async def add_ice_candidate(self, candidate):
|
|
694
|
+
"""Handle incoming ICE candidates."""
|
|
695
|
+
logger.debug(f"Adding remote candidate: {candidate}")
|
|
696
|
+
await self.pc.addIceCandidate(candidate)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024–2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""SmallWebRTC request handler for managing peer connections.
|
|
8
|
+
|
|
9
|
+
This module provides a client for handling web requests and managing WebRTC connections.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from aiortc.sdp import candidate_from_sdp
|
|
18
|
+
from fastapi import HTTPException
|
|
19
|
+
from loguru import logger
|
|
20
|
+
|
|
21
|
+
from pipecat.transports.smallwebrtc.connection import IceServer, SmallWebRTCConnection
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SmallWebRTCRequest:
|
|
26
|
+
"""Small WebRTC transport session arguments for the runner.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
sdp: The SDP string (Session Description Protocol).
|
|
30
|
+
type: The type of the SDP, either "offer" or "answer".
|
|
31
|
+
pc_id: Optional identifier for the peer connection.
|
|
32
|
+
restart_pc: Optional whether to restart the peer connection.
|
|
33
|
+
request_data: Optional custom data sent by the customer.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
sdp: str
|
|
37
|
+
type: str
|
|
38
|
+
pc_id: Optional[str] = None
|
|
39
|
+
restart_pc: Optional[bool] = None
|
|
40
|
+
request_data: Optional[Any] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class IceCandidate:
|
|
45
|
+
"""The remote ice candidate object received from the peer connection.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
candidate: The ice candidate patch SDP string (Session Description Protocol).
|
|
49
|
+
sdp_mid: The SDP mid for the candidate patch.
|
|
50
|
+
sdp_mline_index: The SDP mline index for the candidate patch.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
candidate: str
|
|
54
|
+
sdp_mid: str
|
|
55
|
+
sdp_mline_index: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class SmallWebRTCPatchRequest:
|
|
60
|
+
"""Small WebRTC transport session arguments for the runner.
|
|
61
|
+
|
|
62
|
+
Parameters:
|
|
63
|
+
pc_id: Identifier for the peer connection.
|
|
64
|
+
candidates: A list of ICE candidate patches.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
pc_id: str
|
|
68
|
+
candidates: List[IceCandidate]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ConnectionMode(Enum):
|
|
72
|
+
"""Enum defining the connection handling modes."""
|
|
73
|
+
|
|
74
|
+
SINGLE = "single" # Only one active connection allowed
|
|
75
|
+
MULTIPLE = "multiple" # Multiple simultaneous connections allowed
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SmallWebRTCRequestHandler:
|
|
79
|
+
"""SmallWebRTC request handler for managing peer connections.
|
|
80
|
+
|
|
81
|
+
This class is responsible for:
|
|
82
|
+
- Handling incoming SmallWebRTC requests.
|
|
83
|
+
- Creating and managing WebRTC peer connections.
|
|
84
|
+
- Supporting ESP32-specific SDP munging if enabled.
|
|
85
|
+
- Invoking callbacks for newly initialized connections.
|
|
86
|
+
- Supporting both single and multiple connection modes.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
ice_servers: Optional[List[IceServer]] = None,
|
|
92
|
+
esp32_mode: bool = False,
|
|
93
|
+
host: Optional[str] = None,
|
|
94
|
+
connection_mode: ConnectionMode = ConnectionMode.MULTIPLE,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Initialize a SmallWebRTC request handler.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
ice_servers (Optional[List[IceServer]]): List of ICE servers to use for WebRTC
|
|
100
|
+
connections.
|
|
101
|
+
esp32_mode (bool): If True, enables ESP32-specific SDP munging.
|
|
102
|
+
host (Optional[str]): Host address used for SDP munging in ESP32 mode.
|
|
103
|
+
Ignored if `esp32_mode` is False.
|
|
104
|
+
connection_mode (ConnectionMode): Mode of operation for handling connections.
|
|
105
|
+
SINGLE allows only one active connection, MULTIPLE allows several.
|
|
106
|
+
"""
|
|
107
|
+
self._ice_servers = ice_servers
|
|
108
|
+
self._esp32_mode = esp32_mode
|
|
109
|
+
self._host = host
|
|
110
|
+
self._connection_mode = connection_mode
|
|
111
|
+
|
|
112
|
+
# Store connections by pc_id
|
|
113
|
+
self._pcs_map: Dict[str, SmallWebRTCConnection] = {}
|
|
114
|
+
|
|
115
|
+
def _check_single_connection_constraints(self, pc_id: Optional[str]) -> None:
|
|
116
|
+
"""Check if the connection request satisfies single connection mode constraints.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
pc_id: The peer connection ID from the request
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
HTTPException: If constraints are violated in single connection mode
|
|
123
|
+
"""
|
|
124
|
+
if self._connection_mode != ConnectionMode.SINGLE:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if not self._pcs_map: # No existing connections
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Get the existing connection (should be only one in single mode)
|
|
131
|
+
existing_connection = next(iter(self._pcs_map.values()))
|
|
132
|
+
|
|
133
|
+
if existing_connection.pc_id != pc_id and pc_id:
|
|
134
|
+
logger.warning(
|
|
135
|
+
f"Connection pc_id mismatch: existing={existing_connection.pc_id}, received={pc_id}"
|
|
136
|
+
)
|
|
137
|
+
raise HTTPException(status_code=400, detail="PC ID mismatch with existing connection")
|
|
138
|
+
|
|
139
|
+
if not pc_id:
|
|
140
|
+
logger.warning(
|
|
141
|
+
"Cannot create new connection: existing connection found but no pc_id received"
|
|
142
|
+
)
|
|
143
|
+
raise HTTPException(
|
|
144
|
+
status_code=400,
|
|
145
|
+
detail="Cannot create new connection with existing connection active",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def update_ice_servers(self, ice_servers: Optional[List[IceServer]] = None):
|
|
149
|
+
"""Update the list of ICE servers used for WebRTC connections."""
|
|
150
|
+
self._ice_servers = ice_servers
|
|
151
|
+
|
|
152
|
+
async def handle_web_request(
|
|
153
|
+
self,
|
|
154
|
+
request: SmallWebRTCRequest,
|
|
155
|
+
webrtc_connection_callback: Callable[[Any], Awaitable[None]],
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Handle a SmallWebRTC request and resolve the pending answer.
|
|
158
|
+
|
|
159
|
+
This method will:
|
|
160
|
+
- Reuse an existing WebRTC connection if `pc_id` exists.
|
|
161
|
+
- Otherwise, create a new `SmallWebRTCConnection`.
|
|
162
|
+
- Invoke the provided callback with the connection.
|
|
163
|
+
- Manage ESP32-specific munging if enabled.
|
|
164
|
+
- Enforce single/multiple connection mode constraints.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
request (SmallWebRTCRequest): The incoming WebRTC request, containing
|
|
168
|
+
SDP, type, and optionally a `pc_id`.
|
|
169
|
+
webrtc_connection_callback (Callable[[Any], Awaitable[None]]): An
|
|
170
|
+
asynchronous callback function that is invoked with the WebRTC connection.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
HTTPException: If connection mode constraints are violated
|
|
174
|
+
Exception: Any exception raised during request handling or callback execution
|
|
175
|
+
will be logged and propagated.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
pc_id = request.pc_id
|
|
179
|
+
|
|
180
|
+
# Check connection mode constraints first
|
|
181
|
+
self._check_single_connection_constraints(pc_id)
|
|
182
|
+
|
|
183
|
+
# After constraints are satisfied, get the existing connection if any
|
|
184
|
+
existing_connection = self._pcs_map.get(pc_id) if pc_id else None
|
|
185
|
+
|
|
186
|
+
if existing_connection:
|
|
187
|
+
pipecat_connection = existing_connection
|
|
188
|
+
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
|
|
189
|
+
await pipecat_connection.renegotiate(
|
|
190
|
+
sdp=request.sdp,
|
|
191
|
+
type=request.type,
|
|
192
|
+
restart_pc=request.restart_pc or False,
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
pipecat_connection = SmallWebRTCConnection(ice_servers=self._ice_servers)
|
|
196
|
+
await pipecat_connection.initialize(sdp=request.sdp, type=request.type)
|
|
197
|
+
|
|
198
|
+
@pipecat_connection.event_handler("closed")
|
|
199
|
+
async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
|
|
200
|
+
logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
|
|
201
|
+
self._pcs_map.pop(webrtc_connection.pc_id, None)
|
|
202
|
+
|
|
203
|
+
# Invoke callback provided in runner arguments
|
|
204
|
+
try:
|
|
205
|
+
await webrtc_connection_callback(pipecat_connection)
|
|
206
|
+
logger.debug(
|
|
207
|
+
f"webrtc_connection_callback executed successfully for peer: {pipecat_connection.pc_id}"
|
|
208
|
+
)
|
|
209
|
+
except Exception as callback_error:
|
|
210
|
+
logger.error(
|
|
211
|
+
f"webrtc_connection_callback failed for peer {pipecat_connection.pc_id}: {callback_error}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
answer = pipecat_connection.get_answer()
|
|
215
|
+
|
|
216
|
+
if self._esp32_mode:
|
|
217
|
+
from pipecat.runner.utils import smallwebrtc_sdp_munging
|
|
218
|
+
|
|
219
|
+
answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], self._host)
|
|
220
|
+
|
|
221
|
+
self._pcs_map[answer["pc_id"]] = pipecat_connection
|
|
222
|
+
|
|
223
|
+
return answer
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"Error processing SmallWebRTC request: {e}")
|
|
226
|
+
logger.debug(f"SmallWebRTC request details: {request}")
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
async def handle_patch_request(self, request: SmallWebRTCPatchRequest):
|
|
230
|
+
"""Handle a SmallWebRTC patch candidate request."""
|
|
231
|
+
peer_connection = self._pcs_map.get(request.pc_id)
|
|
232
|
+
|
|
233
|
+
if not peer_connection:
|
|
234
|
+
raise HTTPException(status_code=404, detail="Peer connection not found")
|
|
235
|
+
|
|
236
|
+
for c in request.candidates:
|
|
237
|
+
candidate = candidate_from_sdp(c.candidate)
|
|
238
|
+
candidate.sdpMid = c.sdp_mid
|
|
239
|
+
candidate.sdpMLineIndex = c.sdp_mline_index
|
|
240
|
+
await peer_connection.add_ice_candidate(candidate)
|
|
241
|
+
|
|
242
|
+
async def close(self):
|
|
243
|
+
"""Clear the connection map."""
|
|
244
|
+
coros = [pc.disconnect() for pc in self._pcs_map.values()]
|
|
245
|
+
await asyncio.gather(*coros)
|
|
246
|
+
self._pcs_map.clear()
|