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.
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/METADATA +8 -3
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/RECORD +106 -79
- pipecat/adapters/base_llm_adapter.py +44 -6
- pipecat/adapters/services/anthropic_adapter.py +302 -2
- pipecat/adapters/services/aws_nova_sonic_adapter.py +40 -2
- pipecat/adapters/services/bedrock_adapter.py +40 -2
- pipecat/adapters/services/gemini_adapter.py +276 -6
- pipecat/adapters/services/open_ai_adapter.py +88 -7
- pipecat/adapters/services/open_ai_realtime_adapter.py +39 -1
- pipecat/audio/dtmf/__init__.py +0 -0
- pipecat/audio/dtmf/types.py +47 -0
- pipecat/audio/dtmf/utils.py +70 -0
- pipecat/audio/filters/aic_filter.py +199 -0
- pipecat/audio/utils.py +9 -7
- pipecat/extensions/ivr/__init__.py +0 -0
- pipecat/extensions/ivr/ivr_navigator.py +452 -0
- pipecat/frames/frames.py +156 -43
- pipecat/pipeline/llm_switcher.py +76 -0
- pipecat/pipeline/parallel_pipeline.py +3 -3
- pipecat/pipeline/service_switcher.py +144 -0
- pipecat/pipeline/task.py +68 -28
- pipecat/pipeline/task_observer.py +10 -0
- pipecat/processors/aggregators/dtmf_aggregator.py +2 -2
- pipecat/processors/aggregators/llm_context.py +277 -0
- pipecat/processors/aggregators/llm_response.py +48 -15
- pipecat/processors/aggregators/llm_response_universal.py +840 -0
- pipecat/processors/aggregators/openai_llm_context.py +3 -3
- pipecat/processors/dtmf_aggregator.py +0 -2
- pipecat/processors/filters/stt_mute_filter.py +0 -2
- pipecat/processors/frame_processor.py +18 -11
- pipecat/processors/frameworks/rtvi.py +17 -10
- pipecat/processors/metrics/sentry.py +2 -0
- pipecat/runner/daily.py +137 -36
- pipecat/runner/run.py +1 -1
- pipecat/runner/utils.py +7 -7
- pipecat/serializers/asterisk.py +20 -4
- pipecat/serializers/exotel.py +1 -1
- pipecat/serializers/plivo.py +1 -1
- pipecat/serializers/telnyx.py +1 -1
- pipecat/serializers/twilio.py +1 -1
- pipecat/services/__init__.py +2 -2
- pipecat/services/anthropic/llm.py +113 -28
- pipecat/services/asyncai/tts.py +4 -0
- pipecat/services/aws/llm.py +82 -8
- pipecat/services/aws/tts.py +0 -10
- pipecat/services/aws_nova_sonic/aws.py +5 -0
- pipecat/services/cartesia/tts.py +28 -16
- pipecat/services/cerebras/llm.py +15 -10
- pipecat/services/deepgram/stt.py +8 -0
- pipecat/services/deepseek/llm.py +13 -8
- pipecat/services/fireworks/llm.py +13 -8
- pipecat/services/fish/tts.py +8 -6
- pipecat/services/gemini_multimodal_live/gemini.py +5 -0
- pipecat/services/gladia/config.py +7 -1
- pipecat/services/gladia/stt.py +23 -15
- pipecat/services/google/llm.py +159 -59
- pipecat/services/google/llm_openai.py +18 -3
- pipecat/services/grok/llm.py +2 -1
- pipecat/services/llm_service.py +38 -3
- pipecat/services/mem0/memory.py +2 -1
- pipecat/services/mistral/llm.py +5 -6
- pipecat/services/nim/llm.py +2 -1
- pipecat/services/openai/base_llm.py +88 -26
- pipecat/services/openai/image.py +6 -1
- pipecat/services/openai_realtime_beta/openai.py +5 -2
- pipecat/services/openpipe/llm.py +6 -8
- pipecat/services/perplexity/llm.py +13 -8
- pipecat/services/playht/tts.py +9 -6
- pipecat/services/rime/tts.py +1 -1
- pipecat/services/sambanova/llm.py +18 -13
- pipecat/services/sarvam/tts.py +415 -10
- pipecat/services/speechmatics/stt.py +2 -2
- pipecat/services/tavus/video.py +1 -1
- pipecat/services/tts_service.py +15 -5
- pipecat/services/vistaar/llm.py +2 -5
- pipecat/transports/base_input.py +32 -19
- pipecat/transports/base_output.py +39 -5
- pipecat/transports/daily/__init__.py +0 -0
- pipecat/transports/daily/transport.py +2371 -0
- pipecat/transports/daily/utils.py +410 -0
- pipecat/transports/livekit/__init__.py +0 -0
- pipecat/transports/livekit/transport.py +1042 -0
- pipecat/transports/network/fastapi_websocket.py +12 -546
- pipecat/transports/network/small_webrtc.py +12 -922
- pipecat/transports/network/webrtc_connection.py +9 -595
- pipecat/transports/network/websocket_client.py +12 -481
- pipecat/transports/network/websocket_server.py +12 -487
- pipecat/transports/services/daily.py +9 -2334
- pipecat/transports/services/helpers/daily_rest.py +12 -396
- pipecat/transports/services/livekit.py +12 -975
- pipecat/transports/services/tavus.py +12 -757
- pipecat/transports/smallwebrtc/__init__.py +0 -0
- pipecat/transports/smallwebrtc/connection.py +612 -0
- pipecat/transports/smallwebrtc/transport.py +936 -0
- pipecat/transports/tavus/__init__.py +0 -0
- pipecat/transports/tavus/transport.py +770 -0
- pipecat/transports/websocket/__init__.py +0 -0
- pipecat/transports/websocket/client.py +494 -0
- pipecat/transports/websocket/fastapi.py +559 -0
- pipecat/transports/websocket/server.py +500 -0
- pipecat/transports/whatsapp/__init__.py +0 -0
- pipecat/transports/whatsapp/api.py +345 -0
- pipecat/transports/whatsapp/client.py +364 -0
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
15
|
-
import json
|
|
16
|
-
import time
|
|
17
|
-
from typing import Any, List, Literal, Optional, Union
|
|
14
|
+
import warnings
|
|
18
15
|
|
|
19
|
-
from
|
|
20
|
-
from pydantic import BaseModel, TypeAdapter
|
|
16
|
+
from pipecat.transports.smallwebrtc.connection import *
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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)
|