dv-pipecat-ai 0.0.82.dev815__py3-none-any.whl → 0.0.82.dev857__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.

Files changed (106) hide show
  1. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/METADATA +8 -3
  2. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/RECORD +106 -79
  3. pipecat/adapters/base_llm_adapter.py +44 -6
  4. pipecat/adapters/services/anthropic_adapter.py +302 -2
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +40 -2
  6. pipecat/adapters/services/bedrock_adapter.py +40 -2
  7. pipecat/adapters/services/gemini_adapter.py +276 -6
  8. pipecat/adapters/services/open_ai_adapter.py +88 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +39 -1
  10. pipecat/audio/dtmf/__init__.py +0 -0
  11. pipecat/audio/dtmf/types.py +47 -0
  12. pipecat/audio/dtmf/utils.py +70 -0
  13. pipecat/audio/filters/aic_filter.py +199 -0
  14. pipecat/audio/utils.py +9 -7
  15. pipecat/extensions/ivr/__init__.py +0 -0
  16. pipecat/extensions/ivr/ivr_navigator.py +452 -0
  17. pipecat/frames/frames.py +156 -43
  18. pipecat/pipeline/llm_switcher.py +76 -0
  19. pipecat/pipeline/parallel_pipeline.py +3 -3
  20. pipecat/pipeline/service_switcher.py +144 -0
  21. pipecat/pipeline/task.py +68 -28
  22. pipecat/pipeline/task_observer.py +10 -0
  23. pipecat/processors/aggregators/dtmf_aggregator.py +2 -2
  24. pipecat/processors/aggregators/llm_context.py +277 -0
  25. pipecat/processors/aggregators/llm_response.py +48 -15
  26. pipecat/processors/aggregators/llm_response_universal.py +840 -0
  27. pipecat/processors/aggregators/openai_llm_context.py +3 -3
  28. pipecat/processors/dtmf_aggregator.py +0 -2
  29. pipecat/processors/filters/stt_mute_filter.py +0 -2
  30. pipecat/processors/frame_processor.py +18 -11
  31. pipecat/processors/frameworks/rtvi.py +17 -10
  32. pipecat/processors/metrics/sentry.py +2 -0
  33. pipecat/runner/daily.py +137 -36
  34. pipecat/runner/run.py +1 -1
  35. pipecat/runner/utils.py +7 -7
  36. pipecat/serializers/asterisk.py +20 -4
  37. pipecat/serializers/exotel.py +1 -1
  38. pipecat/serializers/plivo.py +1 -1
  39. pipecat/serializers/telnyx.py +1 -1
  40. pipecat/serializers/twilio.py +1 -1
  41. pipecat/services/__init__.py +2 -2
  42. pipecat/services/anthropic/llm.py +113 -28
  43. pipecat/services/asyncai/tts.py +4 -0
  44. pipecat/services/aws/llm.py +82 -8
  45. pipecat/services/aws/tts.py +0 -10
  46. pipecat/services/aws_nova_sonic/aws.py +5 -0
  47. pipecat/services/cartesia/tts.py +28 -16
  48. pipecat/services/cerebras/llm.py +15 -10
  49. pipecat/services/deepgram/stt.py +8 -0
  50. pipecat/services/deepseek/llm.py +13 -8
  51. pipecat/services/fireworks/llm.py +13 -8
  52. pipecat/services/fish/tts.py +8 -6
  53. pipecat/services/gemini_multimodal_live/gemini.py +5 -0
  54. pipecat/services/gladia/config.py +7 -1
  55. pipecat/services/gladia/stt.py +23 -15
  56. pipecat/services/google/llm.py +159 -59
  57. pipecat/services/google/llm_openai.py +18 -3
  58. pipecat/services/grok/llm.py +2 -1
  59. pipecat/services/llm_service.py +38 -3
  60. pipecat/services/mem0/memory.py +2 -1
  61. pipecat/services/mistral/llm.py +5 -6
  62. pipecat/services/nim/llm.py +2 -1
  63. pipecat/services/openai/base_llm.py +88 -26
  64. pipecat/services/openai/image.py +6 -1
  65. pipecat/services/openai_realtime_beta/openai.py +5 -2
  66. pipecat/services/openpipe/llm.py +6 -8
  67. pipecat/services/perplexity/llm.py +13 -8
  68. pipecat/services/playht/tts.py +9 -6
  69. pipecat/services/rime/tts.py +1 -1
  70. pipecat/services/sambanova/llm.py +18 -13
  71. pipecat/services/sarvam/tts.py +415 -10
  72. pipecat/services/speechmatics/stt.py +2 -2
  73. pipecat/services/tavus/video.py +1 -1
  74. pipecat/services/tts_service.py +15 -5
  75. pipecat/services/vistaar/llm.py +2 -5
  76. pipecat/transports/base_input.py +32 -19
  77. pipecat/transports/base_output.py +39 -5
  78. pipecat/transports/daily/__init__.py +0 -0
  79. pipecat/transports/daily/transport.py +2371 -0
  80. pipecat/transports/daily/utils.py +410 -0
  81. pipecat/transports/livekit/__init__.py +0 -0
  82. pipecat/transports/livekit/transport.py +1042 -0
  83. pipecat/transports/network/fastapi_websocket.py +12 -546
  84. pipecat/transports/network/small_webrtc.py +12 -922
  85. pipecat/transports/network/webrtc_connection.py +9 -595
  86. pipecat/transports/network/websocket_client.py +12 -481
  87. pipecat/transports/network/websocket_server.py +12 -487
  88. pipecat/transports/services/daily.py +9 -2334
  89. pipecat/transports/services/helpers/daily_rest.py +12 -396
  90. pipecat/transports/services/livekit.py +12 -975
  91. pipecat/transports/services/tavus.py +12 -757
  92. pipecat/transports/smallwebrtc/__init__.py +0 -0
  93. pipecat/transports/smallwebrtc/connection.py +612 -0
  94. pipecat/transports/smallwebrtc/transport.py +936 -0
  95. pipecat/transports/tavus/__init__.py +0 -0
  96. pipecat/transports/tavus/transport.py +770 -0
  97. pipecat/transports/websocket/__init__.py +0 -0
  98. pipecat/transports/websocket/client.py +494 -0
  99. pipecat/transports/websocket/fastapi.py +559 -0
  100. pipecat/transports/websocket/server.py +500 -0
  101. pipecat/transports/whatsapp/__init__.py +0 -0
  102. pipecat/transports/whatsapp/api.py +345 -0
  103. pipecat/transports/whatsapp/client.py +364 -0
  104. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/WHEEL +0 -0
  105. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/licenses/LICENSE +0 -0
  106. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/top_level.txt +0 -0
@@ -11,601 +11,15 @@ with support for audio/video tracks, data channels, and signaling
11
11
  for real-time communication applications.
12
12
  """
13
13
 
14
- import asyncio
15
- import json
16
- import time
17
- from typing import Any, List, Literal, Optional, Union
14
+ import warnings
18
15
 
19
- from loguru import logger
20
- from pydantic import BaseModel, TypeAdapter
16
+ from pipecat.transports.smallwebrtc.connection import *
21
17
 
22
- from pipecat.utils.base_object import BaseObject
23
-
24
- try:
25
- from aiortc import (
26
- MediaStreamTrack,
27
- RTCConfiguration,
28
- RTCIceServer,
29
- RTCPeerConnection,
30
- RTCSessionDescription,
18
+ with warnings.catch_warnings():
19
+ warnings.simplefilter("always")
20
+ warnings.warn(
21
+ "Module `pipecat.transports.network.webrtc_connection` is deprecated, "
22
+ "use `pipecat.transports.smallwebrtc.connection` instead.",
23
+ DeprecationWarning,
24
+ stacklevel=2,
31
25
  )
32
- from aiortc.rtcrtpreceiver import RemoteStreamTrack
33
- from av.frame import Frame
34
- except ModuleNotFoundError as e:
35
- logger.error(f"Exception: {e}")
36
- logger.error("In order to use the SmallWebRTC, you need to `pip install pipecat-ai[webrtc]`.")
37
- raise Exception(f"Missing module: {e}")
38
-
39
- SIGNALLING_TYPE = "signalling"
40
- AUDIO_TRANSCEIVER_INDEX = 0
41
- VIDEO_TRANSCEIVER_INDEX = 1
42
- SCREEN_VIDEO_TRANSCEIVER_INDEX = 2
43
-
44
-
45
- class TrackStatusMessage(BaseModel):
46
- """Message for updating track enabled/disabled status.
47
-
48
- Parameters:
49
- type: Message type identifier.
50
- receiver_index: Index of the track receiver to update.
51
- enabled: Whether the track should be enabled or disabled.
52
- """
53
-
54
- type: Literal["trackStatus"]
55
- receiver_index: int
56
- enabled: bool
57
-
58
-
59
- class RenegotiateMessage(BaseModel):
60
- """Message requesting WebRTC renegotiation.
61
-
62
- Parameters:
63
- type: Message type identifier for renegotiation requests.
64
- """
65
-
66
- type: Literal["renegotiate"] = "renegotiate"
67
-
68
-
69
- class PeerLeftMessage(BaseModel):
70
- """Message indicating a peer has left the connection.
71
-
72
- Parameters:
73
- type: Message type identifier for peer departure.
74
- """
75
-
76
- type: Literal["peerLeft"] = "peerLeft"
77
-
78
-
79
- class SignallingMessage:
80
- """Union types for signaling message handling.
81
-
82
- Parameters:
83
- Inbound: Types of messages that can be received from peers.
84
- outbound: Types of messages that can be sent to peers.
85
- """
86
-
87
- Inbound = Union[TrackStatusMessage] # in case we need to add new messages in the future
88
- outbound = Union[RenegotiateMessage]
89
-
90
-
91
- class SmallWebRTCTrack:
92
- """Wrapper for WebRTC media tracks with enabled/disabled state management.
93
-
94
- Provides additional functionality on top of aiortc MediaStreamTrack including
95
- enable/disable control and frame discarding for audio and video streams.
96
- """
97
-
98
- def __init__(self, track: MediaStreamTrack):
99
- """Initialize the WebRTC track wrapper.
100
-
101
- Args:
102
- track: The underlying MediaStreamTrack to wrap.
103
- index: The index of the track in the transceiver (0 for mic, 1 for cam, 2 for screen)
104
- """
105
- self._track = track
106
- self._enabled = True
107
-
108
- def set_enabled(self, enabled: bool) -> None:
109
- """Enable or disable the track.
110
-
111
- Args:
112
- enabled: Whether the track should be enabled for receiving frames.
113
- """
114
- self._enabled = enabled
115
-
116
- def is_enabled(self) -> bool:
117
- """Check if the track is currently enabled.
118
-
119
- Returns:
120
- True if the track is enabled for receiving frames.
121
- """
122
- return self._enabled
123
-
124
- async def discard_old_frames(self):
125
- """Discard old frames from the track queue to reduce latency."""
126
- remote_track = self._track
127
- if isinstance(remote_track, RemoteStreamTrack):
128
- if not hasattr(remote_track, "_queue") or not isinstance(
129
- remote_track._queue, asyncio.Queue
130
- ):
131
- print("Warning: _queue does not exist or has changed in aiortc.")
132
- return
133
- logger.debug("Discarding old frames")
134
- while not remote_track._queue.empty():
135
- remote_track._queue.get_nowait() # Remove the oldest frame
136
- remote_track._queue.task_done()
137
-
138
- async def recv(self) -> Optional[Frame]:
139
- """Receive the next frame from the track.
140
-
141
- Returns:
142
- The next frame, except for video tracks, where it returns the frame only if the track is enabled, otherwise, returns None.
143
- """
144
- if not self._enabled and self._track.kind == "video":
145
- return None
146
- return await self._track.recv()
147
-
148
- def __getattr__(self, name):
149
- """Forward attribute access to the underlying track.
150
-
151
- Args:
152
- name: The attribute name to access.
153
-
154
- Returns:
155
- The attribute value from the underlying track.
156
- """
157
- # Forward other attribute/method calls to the underlying track
158
- return getattr(self._track, name)
159
-
160
-
161
- # Alias so we don't need to expose RTCIceServer
162
- IceServer = RTCIceServer
163
-
164
-
165
- class SmallWebRTCConnection(BaseObject):
166
- """WebRTC connection implementation using aiortc.
167
-
168
- Provides WebRTC peer connection functionality including ICE server configuration,
169
- track management, data channel communication, and connection state handling
170
- for real-time audio/video communication.
171
- """
172
-
173
- def __init__(self, ice_servers: Optional[Union[List[str], List[IceServer]]] = None):
174
- """Initialize the WebRTC connection.
175
-
176
- Args:
177
- ice_servers: List of ICE servers as URLs or IceServer objects.
178
-
179
- Raises:
180
- TypeError: If ice_servers contains mixed types or unsupported types.
181
- """
182
- super().__init__()
183
- if not ice_servers:
184
- self.ice_servers: List[IceServer] = []
185
- elif all(isinstance(s, IceServer) for s in ice_servers):
186
- self.ice_servers = ice_servers
187
- elif all(isinstance(s, str) for s in ice_servers):
188
- self.ice_servers = [IceServer(urls=s) for s in ice_servers]
189
- else:
190
- raise TypeError("ice_servers must be either List[str] or List[RTCIceServer]")
191
- self._connect_invoked = False
192
- self._track_map = {}
193
- self._track_getters = {
194
- AUDIO_TRANSCEIVER_INDEX: self.audio_input_track,
195
- VIDEO_TRANSCEIVER_INDEX: self.video_input_track,
196
- SCREEN_VIDEO_TRANSCEIVER_INDEX: self.screen_video_input_track,
197
- }
198
-
199
- self._initialize()
200
-
201
- # Register supported handlers. The user will only be able to register
202
- # these handlers.
203
- self._register_event_handler("app-message")
204
- self._register_event_handler("track-started")
205
- self._register_event_handler("track-ended")
206
- # connection states
207
- self._register_event_handler("connecting")
208
- self._register_event_handler("connected")
209
- self._register_event_handler("disconnected")
210
- self._register_event_handler("closed")
211
- self._register_event_handler("failed")
212
- self._register_event_handler("new")
213
-
214
- @property
215
- def pc(self) -> RTCPeerConnection:
216
- """Get the underlying RTCPeerConnection.
217
-
218
- Returns:
219
- The aiortc RTCPeerConnection instance.
220
- """
221
- return self._pc
222
-
223
- @property
224
- def pc_id(self) -> str:
225
- """Get the peer connection identifier.
226
-
227
- Returns:
228
- The unique identifier for this peer connection.
229
- """
230
- return self._pc_id
231
-
232
- def _initialize(self):
233
- """Initialize the peer connection and associated components."""
234
- logger.debug("Initializing new peer connection")
235
- rtc_config = RTCConfiguration(iceServers=self.ice_servers)
236
-
237
- self._answer: Optional[RTCSessionDescription] = None
238
- self._pc = RTCPeerConnection(rtc_config)
239
- self._pc_id = self.name
240
- self._setup_listeners()
241
- self._data_channel = None
242
- self._renegotiation_in_progress = False
243
- self._last_received_time = None
244
- self._message_queue = []
245
- self._pending_app_messages = []
246
-
247
- def _setup_listeners(self):
248
- """Set up event listeners for the peer connection."""
249
-
250
- @self._pc.on("datachannel")
251
- def on_datachannel(channel):
252
- self._data_channel = channel
253
-
254
- # Flush queued messages once the data channel is open
255
- @channel.on("open")
256
- async def on_open():
257
- logger.debug("Data channel is open, flushing queued messages")
258
- while self._message_queue:
259
- message = self._message_queue.pop(0)
260
- self._data_channel.send(message)
261
-
262
- @channel.on("message")
263
- async def on_message(message):
264
- try:
265
- # aiortc does not provide any way so we can be aware when we are disconnected,
266
- # so we are using this keep alive message as a way to implement that
267
- if isinstance(message, str) and message.startswith("ping"):
268
- self._last_received_time = time.time()
269
- else:
270
- json_message = json.loads(message)
271
- if json_message["type"] == SIGNALLING_TYPE and json_message.get("message"):
272
- self._handle_signalling_message(json_message["message"])
273
- else:
274
- if self.is_connected():
275
- await self._call_event_handler("app-message", json_message)
276
- else:
277
- logger.debug("Client not connected. Queuing app-message.")
278
- self._pending_app_messages.append(json_message)
279
- except Exception as e:
280
- logger.exception(f"Error parsing JSON message {message}, {e}")
281
-
282
- # Despite the fact that aiortc provides this listener, they don't have a status for "disconnected"
283
- # So, in case we loose connection, this event will not be triggered
284
- @self._pc.on("connectionstatechange")
285
- async def on_connectionstatechange():
286
- await self._handle_new_connection_state()
287
-
288
- # Despite the fact that aiortc provides this listener, they don't have a status for "disconnected"
289
- # So, in case we loose connection, this event will not be triggered
290
- @self._pc.on("iceconnectionstatechange")
291
- async def on_iceconnectionstatechange():
292
- logger.debug(
293
- f"ICE connection state is {self._pc.iceConnectionState}, connection is {self._pc.connectionState}"
294
- )
295
-
296
- @self._pc.on("icegatheringstatechange")
297
- async def on_icegatheringstatechange():
298
- logger.debug(f"ICE gathering state is {self._pc.iceGatheringState}")
299
-
300
- @self._pc.on("track")
301
- async def on_track(track):
302
- logger.debug(f"Track {track.kind} received")
303
- await self._call_event_handler("track-started", track)
304
-
305
- @track.on("ended")
306
- async def on_ended():
307
- logger.debug(f"Track {track.kind} ended")
308
- await self._call_event_handler("track-ended", track)
309
-
310
- async def _create_answer(self, sdp: str, type: str):
311
- """Create an SDP answer for the given offer."""
312
- offer = RTCSessionDescription(sdp=sdp, type=type)
313
- await self._pc.setRemoteDescription(offer)
314
-
315
- # For some reason, aiortc is not respecting the SDP for the transceivers to be sendrcv
316
- # so we are basically forcing it to act this way
317
- self.force_transceivers_to_send_recv()
318
-
319
- # this answer does not contain the ice candidates, which will be gathered later, after the setLocalDescription
320
- logger.debug(f"Creating answer")
321
- local_answer = await self._pc.createAnswer()
322
- await self._pc.setLocalDescription(local_answer)
323
- logger.debug(f"Setting the answer after the local description is created")
324
- self._answer = self._pc.localDescription
325
-
326
- async def initialize(self, sdp: str, type: str):
327
- """Initialize the connection with an SDP offer.
328
-
329
- Args:
330
- sdp: The SDP offer string.
331
- type: The SDP type (usually "offer").
332
- """
333
- await self._create_answer(sdp, type)
334
-
335
- async def connect(self):
336
- """Connect the WebRTC peer connection and handle initial setup."""
337
- self._connect_invoked = True
338
- # If we already connected, trigger again the connected event
339
- if self.is_connected():
340
- await self._call_event_handler("connected")
341
- logger.debug("Flushing pending app-messages")
342
- for message in self._pending_app_messages:
343
- await self._call_event_handler("app-message", message)
344
- # We are renegotiating here, because likely we have loose the first video frames
345
- # and aiortc does not handle that pretty well.
346
- video_input_track = self.video_input_track()
347
- if video_input_track:
348
- await self.video_input_track().discard_old_frames()
349
- screen_video_input_track = self.screen_video_input_track()
350
- if screen_video_input_track:
351
- await self.screen_video_input_track().discard_old_frames()
352
- if video_input_track or screen_video_input_track:
353
- # This prevents an issue where sometimes the WebRTC connection can be established
354
- # before the bot is ready to receive video. When that happens, we can lose a couple
355
- # of seconds of video before we received a key frame to finally start displaying it.
356
- self.ask_to_renegotiate()
357
-
358
- async def renegotiate(self, sdp: str, type: str, restart_pc: bool = False):
359
- """Renegotiate the WebRTC connection with new parameters.
360
-
361
- Args:
362
- sdp: The new SDP offer string.
363
- type: The SDP type (usually "offer").
364
- restart_pc: Whether to restart the peer connection entirely.
365
- """
366
- logger.debug(f"Renegotiating {self._pc_id}")
367
-
368
- if restart_pc:
369
- await self._call_event_handler("disconnected")
370
- logger.debug("Closing old peer connection")
371
- # removing the listeners to prevent the bot from closing
372
- self._pc.remove_all_listeners()
373
- await self._close()
374
- # we are initializing a new peer connection in this case.
375
- self._initialize()
376
-
377
- await self._create_answer(sdp, type)
378
-
379
- # Maybe we should refactor to receive a message from the client side when the renegotiation is completed.
380
- # or look at the peer connection listeners
381
- # but this is good enough for now for testing.
382
- async def delayed_task():
383
- await asyncio.sleep(2)
384
- self._renegotiation_in_progress = False
385
-
386
- asyncio.create_task(delayed_task())
387
-
388
- def force_transceivers_to_send_recv(self):
389
- """Force all transceivers to bidirectional send/receive mode."""
390
- for transceiver in self._pc.getTransceivers():
391
- # For now, we only support sendrecv for camera audio and video (the first two transceivers)
392
- if transceiver.mid == "0" or transceiver.mid == "1":
393
- transceiver.direction = "sendrecv"
394
- else:
395
- transceiver.direction = "recvonly"
396
- # logger.debug(
397
- # f"Transceiver: {transceiver}, Mid: {transceiver.mid}, Direction: {transceiver.direction}"
398
- # )
399
- # logger.debug(f"Sender track: {transceiver.sender.track}")
400
-
401
- def replace_audio_track(self, track):
402
- """Replace the audio track in the first transceiver.
403
-
404
- Args:
405
- track: The new audio track to use for sending.
406
- """
407
- logger.debug(f"Replacing audio track {track.kind}")
408
- # Transceivers always appear in creation-order for both peers
409
- # For now we are only considering that we are going to have 02 transceivers,
410
- # one for audio and one for video
411
- transceivers = self._pc.getTransceivers()
412
- if len(transceivers) > 0 and transceivers[0].sender:
413
- transceivers[0].sender.replaceTrack(track)
414
- else:
415
- logger.warning("Audio transceiver not found. Cannot replace audio track.")
416
-
417
- def replace_video_track(self, track):
418
- """Replace the video track in the second transceiver.
419
-
420
- Args:
421
- track: The new video track to use for sending.
422
- """
423
- logger.debug(f"Replacing video track {track.kind}")
424
- # Transceivers always appear in creation-order for both peers
425
- # For now we are only considering that we are going to have 02 transceivers,
426
- # one for audio and one for video
427
- transceivers = self._pc.getTransceivers()
428
- if len(transceivers) > 1 and transceivers[1].sender:
429
- transceivers[1].sender.replaceTrack(track)
430
- else:
431
- logger.warning("Video transceiver not found. Cannot replace video track.")
432
-
433
- def replace_screen_video_track(self, track):
434
- """Replace the screen video track in the second transceiver.
435
-
436
- Args:
437
- track: The new screen video track to use for sending.
438
- """
439
- logger.debug(f"Replacing screen video track {track.kind}")
440
- # Transceivers always appear in creation-order for both peers
441
- # For now we are only considering that we are going to have 02 transceivers,
442
- # one for audio and one for video
443
- transceivers = self._pc.getTransceivers()
444
- if len(transceivers) > 2 and transceivers[2].sender:
445
- transceivers[2].sender.replaceTrack(track)
446
- else:
447
- logger.warning("Screen video transceiver not found. Cannot replace screen video track.")
448
-
449
- async def disconnect(self):
450
- """Disconnect from the WebRTC peer connection."""
451
- self.send_app_message({"type": SIGNALLING_TYPE, "message": PeerLeftMessage().model_dump()})
452
- await self._close()
453
-
454
- async def _close(self):
455
- """Close the peer connection and cleanup resources."""
456
- if self._pc:
457
- await self._pc.close()
458
- self._message_queue.clear()
459
- self._pending_app_messages.clear()
460
- self._track_map = {}
461
-
462
- def get_answer(self):
463
- """Get the SDP answer for the current connection.
464
-
465
- Returns:
466
- Dictionary containing SDP answer, type, and peer connection ID,
467
- or None if no answer is available.
468
- """
469
- if not self._answer:
470
- return None
471
-
472
- return {
473
- "sdp": self._answer.sdp,
474
- "type": self._answer.type,
475
- "pc_id": self._pc_id,
476
- }
477
-
478
- async def _handle_new_connection_state(self):
479
- """Handle changes in the peer connection state."""
480
- state = self._pc.connectionState
481
- if state == "connected" and not self._connect_invoked:
482
- # We are going to wait until the pipeline is ready before triggering the event
483
- return
484
- logger.debug(f"Connection state changed to: {state}")
485
- await self._call_event_handler(state)
486
- if state == "failed":
487
- logger.warning("Connection failed, closing peer connection.")
488
- await self._close()
489
-
490
- # Despite the fact that aiortc provides this listener, they don't have a status for "disconnected"
491
- # So, there is no advantage in looking at self._pc.connectionState
492
- # That is why we are trying to keep our own state
493
- def is_connected(self) -> bool:
494
- """Check if the WebRTC connection is currently active.
495
-
496
- Returns:
497
- True if the connection is active and receiving data.
498
- """
499
- # If the small webrtc transport has never invoked to connect
500
- # we are acting like if we are not connected
501
- if not self._connect_invoked:
502
- return False
503
-
504
- if self._last_received_time is None:
505
- # if we have never received a message, it is probably because the client has not created a data channel
506
- # so we are going to trust aiortc in this case
507
- return self._pc.connectionState == "connected"
508
- # Checks if the last received ping was within the last 3 seconds.
509
- return (time.time() - self._last_received_time) < 3
510
-
511
- def audio_input_track(self):
512
- """Get the audio input track wrapper.
513
-
514
- Returns:
515
- SmallWebRTCTrack wrapper for the audio track, or None if unavailable.
516
- """
517
- if self._track_map.get(AUDIO_TRANSCEIVER_INDEX):
518
- return self._track_map[AUDIO_TRANSCEIVER_INDEX]
519
-
520
- # Transceivers always appear in creation-order for both peers
521
- # For support 3 receivers in the following order:
522
- # audio, video, screenVideo
523
- transceivers = self._pc.getTransceivers()
524
- if len(transceivers) == 0 or not transceivers[AUDIO_TRANSCEIVER_INDEX].receiver:
525
- logger.warning("No audio transceiver is available")
526
- return None
527
-
528
- track = transceivers[AUDIO_TRANSCEIVER_INDEX].receiver.track
529
- audio_track = SmallWebRTCTrack(track) if track else None
530
- self._track_map[AUDIO_TRANSCEIVER_INDEX] = audio_track
531
- return audio_track
532
-
533
- def video_input_track(self):
534
- """Get the video input track wrapper.
535
-
536
- Returns:
537
- SmallWebRTCTrack wrapper for the video track, or None if unavailable.
538
- """
539
- if self._track_map.get(VIDEO_TRANSCEIVER_INDEX):
540
- return self._track_map[VIDEO_TRANSCEIVER_INDEX]
541
-
542
- # Transceivers always appear in creation-order for both peers
543
- # For support 3 receivers in the following order:
544
- # audio, video, screenVideo
545
- transceivers = self._pc.getTransceivers()
546
- if len(transceivers) <= 1 or not transceivers[VIDEO_TRANSCEIVER_INDEX].receiver:
547
- logger.warning("No video transceiver is available")
548
- return None
549
-
550
- track = transceivers[VIDEO_TRANSCEIVER_INDEX].receiver.track
551
- video_track = SmallWebRTCTrack(track) if track else None
552
- self._track_map[VIDEO_TRANSCEIVER_INDEX] = video_track
553
- return video_track
554
-
555
- def screen_video_input_track(self):
556
- """Get the screen video input track wrapper.
557
-
558
- Returns:
559
- SmallWebRTCTrack wrapper for the screen video track, or None if unavailable.
560
- """
561
- if self._track_map.get(SCREEN_VIDEO_TRANSCEIVER_INDEX):
562
- return self._track_map[SCREEN_VIDEO_TRANSCEIVER_INDEX]
563
-
564
- # Transceivers always appear in creation-order for both peers
565
- # For support 3 receivers in the following order:
566
- # audio, video, screenVideo
567
- transceivers = self._pc.getTransceivers()
568
- if len(transceivers) <= 2 or not transceivers[SCREEN_VIDEO_TRANSCEIVER_INDEX].receiver:
569
- logger.warning("No screen video transceiver is available")
570
- return None
571
-
572
- track = transceivers[SCREEN_VIDEO_TRANSCEIVER_INDEX].receiver.track
573
- video_track = SmallWebRTCTrack(track) if track else None
574
- self._track_map[SCREEN_VIDEO_TRANSCEIVER_INDEX] = video_track
575
- return video_track
576
-
577
- def send_app_message(self, message: Any):
578
- """Send an application message through the data channel.
579
-
580
- Args:
581
- message: The message to send (will be JSON serialized).
582
- """
583
- json_message = json.dumps(message)
584
- if self._data_channel and self._data_channel.readyState == "open":
585
- self._data_channel.send(json_message)
586
- else:
587
- logger.debug("Data channel not ready, queuing message")
588
- self._message_queue.append(json_message)
589
-
590
- def ask_to_renegotiate(self):
591
- """Request renegotiation of the WebRTC connection."""
592
- if self._renegotiation_in_progress:
593
- return
594
-
595
- self._renegotiation_in_progress = True
596
- self.send_app_message(
597
- {"type": SIGNALLING_TYPE, "message": RenegotiateMessage().model_dump()}
598
- )
599
-
600
- def _handle_signalling_message(self, message):
601
- """Handle incoming signaling messages."""
602
- logger.debug(f"Signalling message received: {message}")
603
- inbound_adapter = TypeAdapter(SignallingMessage.Inbound)
604
- signalling_message = inbound_adapter.validate_python(message)
605
- match signalling_message:
606
- case TrackStatusMessage():
607
- track = (
608
- self._track_getters.get(signalling_message.receiver_index) or (lambda: None)
609
- )()
610
- if track:
611
- track.set_enabled(signalling_message.enabled)