livekit 1.2.1__py3-none-win_arm64.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.
- livekit/rtc/__init__.py +264 -0
- livekit/rtc/_ffi_client.py +321 -0
- livekit/rtc/_proto/__init__.py +0 -0
- livekit/rtc/_proto/audio_frame_pb2.py +159 -0
- livekit/rtc/_proto/audio_frame_pb2.pyi +1560 -0
- livekit/rtc/_proto/capture_pb2.py +70 -0
- livekit/rtc/_proto/capture_pb2.pyi +559 -0
- livekit/rtc/_proto/data_stream_pb2.py +171 -0
- livekit/rtc/_proto/data_stream_pb2.pyi +1498 -0
- livekit/rtc/_proto/data_track_pb2.py +120 -0
- livekit/rtc/_proto/data_track_pb2.pyi +1073 -0
- livekit/rtc/_proto/e2ee_pb2.py +81 -0
- livekit/rtc/_proto/e2ee_pb2.pyi +591 -0
- livekit/rtc/_proto/ffi_pb2.py +55 -0
- livekit/rtc/_proto/ffi_pb2.pyi +1246 -0
- livekit/rtc/_proto/handle_pb2.py +27 -0
- livekit/rtc/_proto/handle_pb2.pyi +51 -0
- livekit/rtc/_proto/participant_pb2.py +45 -0
- livekit/rtc/_proto/participant_pb2.pyi +313 -0
- livekit/rtc/_proto/room_pb2.py +311 -0
- livekit/rtc/_proto/room_pb2.pyi +3136 -0
- livekit/rtc/_proto/rpc_pb2.py +47 -0
- livekit/rtc/_proto/rpc_pb2.pyi +261 -0
- livekit/rtc/_proto/stats_pb2.py +123 -0
- livekit/rtc/_proto/stats_pb2.pyi +1514 -0
- livekit/rtc/_proto/track_pb2.py +76 -0
- livekit/rtc/_proto/track_pb2.pyi +544 -0
- livekit/rtc/_proto/track_publication_pb2.py +39 -0
- livekit/rtc/_proto/track_publication_pb2.pyi +145 -0
- livekit/rtc/_proto/video_frame_pb2.py +97 -0
- livekit/rtc/_proto/video_frame_pb2.pyi +838 -0
- livekit/rtc/_utils.py +187 -0
- livekit/rtc/apm.py +119 -0
- livekit/rtc/audio_filter.py +23 -0
- livekit/rtc/audio_frame.py +242 -0
- livekit/rtc/audio_mixer.py +247 -0
- livekit/rtc/audio_resampler.py +174 -0
- livekit/rtc/audio_source.py +191 -0
- livekit/rtc/audio_stream.py +354 -0
- livekit/rtc/data_stream.py +640 -0
- livekit/rtc/data_track.py +296 -0
- livekit/rtc/e2ee.py +320 -0
- livekit/rtc/encoded_video.py +127 -0
- livekit/rtc/event_emitter.py +215 -0
- livekit/rtc/frame_processor.py +37 -0
- livekit/rtc/jupyter.py +68 -0
- livekit/rtc/log.py +3 -0
- livekit/rtc/media_devices.py +596 -0
- livekit/rtc/participant.py +1171 -0
- livekit/rtc/platform_audio.py +426 -0
- livekit/rtc/py.typed +0 -0
- livekit/rtc/resources/LICENSE.md +4055 -0
- livekit/rtc/resources/__init__.py +1 -0
- livekit/rtc/resources/jupyter-html/index.html +101 -0
- livekit/rtc/resources/livekit_ffi.dll +0 -0
- livekit/rtc/resources/livekit_ffi.h +42 -0
- livekit/rtc/room.py +1304 -0
- livekit/rtc/rpc.py +244 -0
- livekit/rtc/synchronizer.py +202 -0
- livekit/rtc/track.py +264 -0
- livekit/rtc/track_publication.py +145 -0
- livekit/rtc/transcription.py +19 -0
- livekit/rtc/utils.py +138 -0
- livekit/rtc/version.py +1 -0
- livekit/rtc/video_frame.py +347 -0
- livekit/rtc/video_source.py +74 -0
- livekit/rtc/video_stream.py +191 -0
- livekit-1.2.1.dist-info/METADATA +80 -0
- livekit-1.2.1.dist-info/RECORD +71 -0
- livekit-1.2.1.dist-info/WHEEL +5 -0
- livekit-1.2.1.dist-info/top_level.txt +1 -0
livekit/rtc/track.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Copyright 2023 LiveKit, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import weakref
|
|
18
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
|
19
|
+
from ._ffi_client import FfiHandle, FfiClient
|
|
20
|
+
from ._proto import ffi_pb2 as proto_ffi
|
|
21
|
+
from ._proto import track_pb2 as proto_track
|
|
22
|
+
from ._proto import stats_pb2 as proto_stats
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from .audio_source import AudioSource
|
|
26
|
+
from .audio_stream import AudioStream
|
|
27
|
+
from .room import Room
|
|
28
|
+
from .video_source import VideoSource
|
|
29
|
+
from .encoded_video import EncodedVideoSource
|
|
30
|
+
from .platform_audio import PlatformAudioSource
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Track:
|
|
34
|
+
def __init__(self, owned_info: proto_track.OwnedTrack):
|
|
35
|
+
self._info = owned_info.info
|
|
36
|
+
self._ffi_handle = FfiHandle(owned_info.handle.id)
|
|
37
|
+
self._room_ref: Optional[weakref.ref[Room]] = None
|
|
38
|
+
self._audio_streams: weakref.WeakSet[AudioStream] = weakref.WeakSet()
|
|
39
|
+
|
|
40
|
+
def _resolve_room(self) -> Optional[Room]:
|
|
41
|
+
return self._room_ref() if self._room_ref is not None else None
|
|
42
|
+
|
|
43
|
+
def _set_room(self, room: Optional[Room]) -> None:
|
|
44
|
+
old_room = self._resolve_room()
|
|
45
|
+
if old_room is None and room is None:
|
|
46
|
+
# Already roomless — nothing to detach and nothing to re-clear.
|
|
47
|
+
# Without this guard a second _set_room(None) (e.g. the unpublish /
|
|
48
|
+
# local_track_unpublished race calling it from both paths) would
|
|
49
|
+
# re-fire _on_*_cleared on every registered processor.
|
|
50
|
+
return
|
|
51
|
+
if old_room is not room:
|
|
52
|
+
if old_room is not None:
|
|
53
|
+
old_room.off("token_refreshed", self._on_room_token_refreshed)
|
|
54
|
+
if room is not None:
|
|
55
|
+
room.on("token_refreshed", self._on_room_token_refreshed)
|
|
56
|
+
|
|
57
|
+
self._room_ref = weakref.ref(room) if room is not None else None
|
|
58
|
+
|
|
59
|
+
for stream in self._audio_streams:
|
|
60
|
+
self._push_processor_metadata_to_stream(stream, room)
|
|
61
|
+
|
|
62
|
+
def _on_room_token_refreshed(self) -> None:
|
|
63
|
+
room = self._resolve_room()
|
|
64
|
+
if room is None or room._token is None or room._server_url is None:
|
|
65
|
+
return
|
|
66
|
+
for stream in self._audio_streams:
|
|
67
|
+
if not stream._processor:
|
|
68
|
+
continue
|
|
69
|
+
stream._processor._on_credentials_updated(token=room._token, url=room._server_url)
|
|
70
|
+
|
|
71
|
+
def _push_processor_metadata_to_stream(self, stream: AudioStream, room: Optional[Room]) -> None:
|
|
72
|
+
if not stream._processor:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if room is None:
|
|
76
|
+
# track left a room - clear processor's room context
|
|
77
|
+
stream._processor._on_stream_info_cleared()
|
|
78
|
+
stream._processor._on_credentials_cleared()
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
identity = ""
|
|
82
|
+
pub_sid = ""
|
|
83
|
+
track_sid = self.sid
|
|
84
|
+
if track_sid:
|
|
85
|
+
for participant in room.remote_participants.values():
|
|
86
|
+
publication = participant.track_publications.get(track_sid)
|
|
87
|
+
if publication is not None:
|
|
88
|
+
identity, pub_sid = participant.identity, publication.sid
|
|
89
|
+
break
|
|
90
|
+
else:
|
|
91
|
+
local = room._local_participant
|
|
92
|
+
if local is not None:
|
|
93
|
+
for local_publication in local.track_publications.values():
|
|
94
|
+
if local_publication.sid == track_sid:
|
|
95
|
+
identity, pub_sid = local.identity, local_publication.sid
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
stream._processor._on_stream_info_updated(
|
|
99
|
+
room_name=room.name,
|
|
100
|
+
participant_identity=identity,
|
|
101
|
+
publication_sid=pub_sid,
|
|
102
|
+
)
|
|
103
|
+
if room._token is not None and room._server_url is not None:
|
|
104
|
+
stream._processor._on_credentials_updated(token=room._token, url=room._server_url)
|
|
105
|
+
|
|
106
|
+
def _register_audio_stream(self, stream: AudioStream) -> None:
|
|
107
|
+
self._audio_streams.add(stream)
|
|
108
|
+
room = self._resolve_room()
|
|
109
|
+
if room is not None:
|
|
110
|
+
self._push_processor_metadata_to_stream(stream, room)
|
|
111
|
+
|
|
112
|
+
def _unregister_audio_stream(self, stream: AudioStream) -> None:
|
|
113
|
+
self._audio_streams.discard(stream)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def sid(self) -> str:
|
|
117
|
+
return self._info.sid
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def name(self) -> str:
|
|
121
|
+
return self._info.name
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def kind(self) -> proto_track.TrackKind.ValueType:
|
|
125
|
+
return self._info.kind
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def stream_state(self) -> proto_track.StreamState.ValueType:
|
|
129
|
+
return self._info.stream_state
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def muted(self) -> bool:
|
|
133
|
+
return self._info.muted
|
|
134
|
+
|
|
135
|
+
async def get_stats(self) -> List[proto_stats.RtcStats]:
|
|
136
|
+
req = proto_ffi.FfiRequest()
|
|
137
|
+
req.get_stats.track_handle = self._ffi_handle.handle
|
|
138
|
+
|
|
139
|
+
queue = FfiClient.instance.queue.subscribe()
|
|
140
|
+
try:
|
|
141
|
+
resp = FfiClient.instance.request(req)
|
|
142
|
+
cb: proto_ffi.FfiEvent = await queue.wait_for(
|
|
143
|
+
lambda e: e.get_stats.async_id == resp.get_stats.async_id
|
|
144
|
+
)
|
|
145
|
+
finally:
|
|
146
|
+
FfiClient.instance.queue.unsubscribe(queue)
|
|
147
|
+
|
|
148
|
+
if cb.get_stats.error:
|
|
149
|
+
raise Exception(cb.get_stats.error)
|
|
150
|
+
|
|
151
|
+
return list(cb.get_stats.stats)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class LocalAudioTrack(Track):
|
|
155
|
+
def __init__(self, info: proto_track.OwnedTrack):
|
|
156
|
+
super().__init__(info)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def create_audio_track(
|
|
160
|
+
name: str, source: Union["AudioSource", "PlatformAudioSource"]
|
|
161
|
+
) -> "LocalAudioTrack":
|
|
162
|
+
"""Create a local audio track from an audio source.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: The name of the track (e.g., "microphone", "audio-file").
|
|
166
|
+
source: Either an AudioSource (synthetic mode for manual frame capture)
|
|
167
|
+
or a PlatformAudioSource (from PlatformAudio, uses WebRTC ADM).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
A LocalAudioTrack that can be published to a room.
|
|
171
|
+
|
|
172
|
+
Example with PlatformAudio (recommended for most use cases):
|
|
173
|
+
```python
|
|
174
|
+
platform_audio = rtc.PlatformAudio()
|
|
175
|
+
source = platform_audio.create_audio_source()
|
|
176
|
+
track = rtc.LocalAudioTrack.create_audio_track("microphone", source)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Example with AudioSource (synthetic mode for custom processing):
|
|
180
|
+
```python
|
|
181
|
+
# Synthetic mode: You must manually capture frames
|
|
182
|
+
source = rtc.AudioSource(sample_rate=48000, num_channels=1)
|
|
183
|
+
track = rtc.LocalAudioTrack.create_audio_track("audio", source)
|
|
184
|
+
# Then in a loop: await source.capture_frame(frame)
|
|
185
|
+
```
|
|
186
|
+
"""
|
|
187
|
+
req = proto_ffi.FfiRequest()
|
|
188
|
+
req.create_audio_track.name = name
|
|
189
|
+
req.create_audio_track.source_handle = source._ffi_handle.handle
|
|
190
|
+
|
|
191
|
+
resp = FfiClient.instance.request(req)
|
|
192
|
+
return LocalAudioTrack(resp.create_audio_track.track)
|
|
193
|
+
|
|
194
|
+
def mute(self) -> None:
|
|
195
|
+
req = proto_ffi.FfiRequest()
|
|
196
|
+
req.local_track_mute.track_handle = self._ffi_handle.handle
|
|
197
|
+
req.local_track_mute.mute = True
|
|
198
|
+
FfiClient.instance.request(req)
|
|
199
|
+
self._info.muted = True
|
|
200
|
+
|
|
201
|
+
def unmute(self) -> None:
|
|
202
|
+
req = proto_ffi.FfiRequest()
|
|
203
|
+
req.local_track_mute.track_handle = self._ffi_handle.handle
|
|
204
|
+
req.local_track_mute.mute = False
|
|
205
|
+
FfiClient.instance.request(req)
|
|
206
|
+
self._info.muted = False
|
|
207
|
+
|
|
208
|
+
def __repr__(self) -> str:
|
|
209
|
+
return f"rtc.LocalAudioTrack(sid={self.sid}, name={self.name})"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class LocalVideoTrack(Track):
|
|
213
|
+
def __init__(self, info: proto_track.OwnedTrack):
|
|
214
|
+
super().__init__(info)
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def create_video_track(
|
|
218
|
+
name: str, source: "Union[VideoSource, EncodedVideoSource]"
|
|
219
|
+
) -> "LocalVideoTrack":
|
|
220
|
+
req = proto_ffi.FfiRequest()
|
|
221
|
+
req.create_video_track.name = name
|
|
222
|
+
req.create_video_track.source_handle = source._ffi_handle.handle
|
|
223
|
+
|
|
224
|
+
resp = FfiClient.instance.request(req)
|
|
225
|
+
return LocalVideoTrack(resp.create_video_track.track)
|
|
226
|
+
|
|
227
|
+
def mute(self) -> None:
|
|
228
|
+
req = proto_ffi.FfiRequest()
|
|
229
|
+
req.local_track_mute.track_handle = self._ffi_handle.handle
|
|
230
|
+
req.local_track_mute.mute = True
|
|
231
|
+
FfiClient.instance.request(req)
|
|
232
|
+
self._info.muted = True
|
|
233
|
+
|
|
234
|
+
def unmute(self) -> None:
|
|
235
|
+
req = proto_ffi.FfiRequest()
|
|
236
|
+
req.local_track_mute.track_handle = self._ffi_handle.handle
|
|
237
|
+
req.local_track_mute.mute = False
|
|
238
|
+
FfiClient.instance.request(req)
|
|
239
|
+
self._info.muted = False
|
|
240
|
+
|
|
241
|
+
def __repr__(self) -> str:
|
|
242
|
+
return f"rtc.LocalVideoTrack(sid={self.sid}, name={self.name})"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class RemoteAudioTrack(Track):
|
|
246
|
+
def __init__(self, info: proto_track.OwnedTrack):
|
|
247
|
+
super().__init__(info)
|
|
248
|
+
|
|
249
|
+
def __repr__(self) -> str:
|
|
250
|
+
return f"rtc.RemoteAudioTrack(sid={self.sid}, name={self.name})"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class RemoteVideoTrack(Track):
|
|
254
|
+
def __init__(self, info: proto_track.OwnedTrack):
|
|
255
|
+
super().__init__(info)
|
|
256
|
+
|
|
257
|
+
def __repr__(self) -> str:
|
|
258
|
+
return f"rtc.RemoteVideoTrack(sid={self.sid}, name={self.name})"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
LocalTrack = Union[LocalVideoTrack, LocalAudioTrack]
|
|
262
|
+
RemoteTrack = Union[RemoteVideoTrack, RemoteAudioTrack]
|
|
263
|
+
AudioTrack = Union[LocalAudioTrack, RemoteAudioTrack]
|
|
264
|
+
VideoTrack = Union[LocalVideoTrack, RemoteVideoTrack]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright 2023 LiveKit, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import List, Optional, cast
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
from ._ffi_client import FfiHandle, FfiClient
|
|
19
|
+
from ._proto import e2ee_pb2 as proto_e2ee
|
|
20
|
+
from ._proto import ffi_pb2 as proto_ffi
|
|
21
|
+
from ._proto import track_pb2 as proto_track
|
|
22
|
+
from ._proto import track_publication_pb2 as proto_track_pub
|
|
23
|
+
from .track import Track, LocalTrack, RemoteTrack
|
|
24
|
+
|
|
25
|
+
VideoQuality = proto_track_pub.VideoQuality
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TrackPublication:
|
|
29
|
+
def __init__(self, owned_info: proto_track.OwnedTrackPublication):
|
|
30
|
+
self._info = owned_info.info
|
|
31
|
+
self._track: Optional[Track] = None
|
|
32
|
+
self._ffi_handle = FfiHandle(owned_info.handle.id)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def track(self) -> Optional[Track]:
|
|
36
|
+
return self._track
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def sid(self) -> str:
|
|
40
|
+
return self._info.sid
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def name(self) -> str:
|
|
44
|
+
return self._info.name
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def kind(self) -> proto_track.TrackKind.ValueType:
|
|
48
|
+
return self._info.kind
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def source(self) -> proto_track.TrackSource.ValueType:
|
|
52
|
+
return self._info.source
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def simulcasted(self) -> bool:
|
|
56
|
+
return self._info.simulcasted
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def width(self) -> int:
|
|
60
|
+
return self._info.width
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def height(self) -> int:
|
|
64
|
+
return self._info.height
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def mime_type(self) -> str:
|
|
68
|
+
return self._info.mime_type
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def muted(self) -> bool:
|
|
72
|
+
return self._info.muted
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def encryption_type(self) -> proto_e2ee.EncryptionType.ValueType:
|
|
76
|
+
return self._info.encryption_type
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def audio_features(self) -> List[proto_track.AudioTrackFeature.ValueType]:
|
|
80
|
+
return list(self._info.audio_features)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def frame_metadata_features(self) -> List[proto_track.FrameMetadataFeature.ValueType]:
|
|
84
|
+
return list(self._info.frame_metadata_features)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LocalTrackPublication(TrackPublication):
|
|
88
|
+
def __init__(self, owned_info: proto_track.OwnedTrackPublication):
|
|
89
|
+
super().__init__(owned_info)
|
|
90
|
+
self._first_subscription: asyncio.Future[None] = asyncio.Future()
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def track(self) -> Optional[LocalTrack]:
|
|
94
|
+
return cast(Optional[LocalTrack], self._track)
|
|
95
|
+
|
|
96
|
+
async def wait_for_subscription(self) -> None:
|
|
97
|
+
await asyncio.shield(self._first_subscription)
|
|
98
|
+
|
|
99
|
+
def __repr__(self) -> str:
|
|
100
|
+
return f"rtc.LocalTrackPublication(sid={self.sid}, name={self.name}, kind={self.kind}, source={self.source})"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RemoteTrackPublication(TrackPublication):
|
|
104
|
+
def __init__(self, owned_info: proto_track.OwnedTrackPublication):
|
|
105
|
+
super().__init__(owned_info)
|
|
106
|
+
self._subscribed = False
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def track(self) -> Optional[RemoteTrack]:
|
|
110
|
+
return cast(Optional[RemoteTrack], self._track)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def subscribed(self) -> bool:
|
|
114
|
+
return self._subscribed
|
|
115
|
+
|
|
116
|
+
def set_subscribed(self, subscribed: bool) -> None:
|
|
117
|
+
req = proto_ffi.FfiRequest()
|
|
118
|
+
req.set_subscribed.subscribe = subscribed
|
|
119
|
+
req.set_subscribed.publication_handle = self._ffi_handle.handle
|
|
120
|
+
FfiClient.instance.request(req)
|
|
121
|
+
|
|
122
|
+
def set_video_quality(self, quality: "VideoQuality.ValueType") -> None:
|
|
123
|
+
"""For simulcasted video tracks, request a specific simulcast layer
|
|
124
|
+
from the server. Use one of `rtc.VideoQuality.VIDEO_QUALITY_LOW` (q),
|
|
125
|
+
`VIDEO_QUALITY_MEDIUM` (h), or `VIDEO_QUALITY_HIGH` (f).
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: if the publication is not a video track, or is not
|
|
129
|
+
simulcasted (selecting a layer requires multiple layers).
|
|
130
|
+
"""
|
|
131
|
+
if self.kind != proto_track.TrackKind.KIND_VIDEO:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"set_video_quality is only supported on video publications (sid={self.sid})"
|
|
134
|
+
)
|
|
135
|
+
if not self.simulcasted:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"set_video_quality requires a simulcasted publication (sid={self.sid})"
|
|
138
|
+
)
|
|
139
|
+
req = proto_ffi.FfiRequest()
|
|
140
|
+
req.set_remote_track_publication_quality.track_publication_handle = self._ffi_handle.handle
|
|
141
|
+
req.set_remote_track_publication_quality.quality = quality
|
|
142
|
+
FfiClient.instance.request(req)
|
|
143
|
+
|
|
144
|
+
def __repr__(self) -> str:
|
|
145
|
+
return f"rtc.RemoteTrackPublication(sid={self.sid}, name={self.name}, kind={self.kind}, source={self.source})"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Transcription:
|
|
7
|
+
participant_identity: str
|
|
8
|
+
track_sid: str
|
|
9
|
+
segments: List["TranscriptionSegment"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TranscriptionSegment:
|
|
14
|
+
id: str
|
|
15
|
+
text: str
|
|
16
|
+
start_time: int
|
|
17
|
+
end_time: int
|
|
18
|
+
language: str
|
|
19
|
+
final: bool
|
livekit/rtc/utils.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import AsyncIterator
|
|
4
|
+
|
|
5
|
+
from .audio_frame import AudioFrame
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ["combine_audio_frames", "sine_wave_generator"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def combine_audio_frames(buffer: AudioFrame | list[AudioFrame]) -> AudioFrame:
|
|
12
|
+
"""
|
|
13
|
+
Combines one or more `rtc.AudioFrame` objects into a single `rtc.AudioFrame`.
|
|
14
|
+
|
|
15
|
+
This function concatenates the audio data from multiple frames, ensuring that
|
|
16
|
+
all frames have the same sample rate and number of channels. It efficiently
|
|
17
|
+
merges the data by preallocating the necessary memory and copying the frame
|
|
18
|
+
data without unnecessary reallocations.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
buffer: A single `rtc.AudioFrame` or a list of `rtc.AudioFrame`
|
|
22
|
+
objects to be combined.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
rtc.AudioFrame: A new `rtc.AudioFrame` containing the combined audio data.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If the buffer is empty.
|
|
29
|
+
ValueError: If frames have differing sample rates.
|
|
30
|
+
ValueError: If frames have differing numbers of channels.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> frame1 = rtc.AudioFrame(
|
|
34
|
+
... data=b"\x01\x02", sample_rate=48000, num_channels=2, samples_per_channel=1
|
|
35
|
+
... )
|
|
36
|
+
>>> frame2 = rtc.AudioFrame(
|
|
37
|
+
... data=b"\x03\x04", sample_rate=48000, num_channels=2, samples_per_channel=1
|
|
38
|
+
... )
|
|
39
|
+
>>> combined_frame = combine_audio_frames([frame1, frame2])
|
|
40
|
+
>>> combined_frame.data
|
|
41
|
+
b'\x01\x02\x03\x04'
|
|
42
|
+
>>> combined_frame.sample_rate
|
|
43
|
+
48000
|
|
44
|
+
>>> combined_frame.num_channels
|
|
45
|
+
2
|
|
46
|
+
>>> combined_frame.samples_per_channel
|
|
47
|
+
2
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(buffer, list):
|
|
50
|
+
return buffer
|
|
51
|
+
|
|
52
|
+
if not buffer:
|
|
53
|
+
raise ValueError("buffer is empty")
|
|
54
|
+
|
|
55
|
+
sample_rate = buffer[0].sample_rate
|
|
56
|
+
num_channels = buffer[0].num_channels
|
|
57
|
+
|
|
58
|
+
total_data_length = 0
|
|
59
|
+
total_samples_per_channel = 0
|
|
60
|
+
|
|
61
|
+
for frame in buffer:
|
|
62
|
+
if frame.sample_rate != sample_rate:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Sample rate mismatch: expected {sample_rate}, got {frame.sample_rate}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if frame.num_channels != num_channels:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Channel count mismatch: expected {num_channels}, got {frame.num_channels}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
total_data_length += len(frame.data)
|
|
73
|
+
total_samples_per_channel += frame.samples_per_channel
|
|
74
|
+
|
|
75
|
+
data = bytearray(total_data_length)
|
|
76
|
+
offset = 0
|
|
77
|
+
for frame in buffer:
|
|
78
|
+
frame_data = frame.data.cast("b")
|
|
79
|
+
data[offset : offset + len(frame_data)] = frame_data
|
|
80
|
+
offset += len(frame_data)
|
|
81
|
+
|
|
82
|
+
return AudioFrame(
|
|
83
|
+
data=data,
|
|
84
|
+
sample_rate=sample_rate,
|
|
85
|
+
num_channels=num_channels,
|
|
86
|
+
samples_per_channel=total_samples_per_channel,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def sine_wave_generator(
|
|
91
|
+
freq: float,
|
|
92
|
+
duration: float,
|
|
93
|
+
sample_rate: int = 48000,
|
|
94
|
+
amplitude: float = 0.3,
|
|
95
|
+
) -> AsyncIterator[AudioFrame]:
|
|
96
|
+
"""
|
|
97
|
+
Generate sine wave audio frames.
|
|
98
|
+
|
|
99
|
+
Useful for testing audio pipelines and generating test signals.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
freq: Frequency of the sine wave in Hz.
|
|
103
|
+
duration: Duration of the audio in seconds.
|
|
104
|
+
sample_rate: Sample rate in Hz (default: 48000).
|
|
105
|
+
amplitude: Amplitude of the sine wave, range [0.0, 1.0] (default: 0.3).
|
|
106
|
+
|
|
107
|
+
Yields:
|
|
108
|
+
AudioFrame: Audio frames containing sine wave data.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> import asyncio
|
|
112
|
+
>>> async def generate_audio():
|
|
113
|
+
... async for frame in sine_wave_generator(440, 1.0):
|
|
114
|
+
... print(f"Generated frame with {frame.samples_per_channel} samples")
|
|
115
|
+
>>> asyncio.run(generate_audio())
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
import numpy as np
|
|
119
|
+
except ImportError:
|
|
120
|
+
raise ImportError(
|
|
121
|
+
"numpy is required for sine_wave_generator. Install it with: pip install numpy"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
blocksize = sample_rate // 10
|
|
125
|
+
total_frames = int((duration * sample_rate) // blocksize)
|
|
126
|
+
t_frame = np.arange(blocksize) / sample_rate
|
|
127
|
+
|
|
128
|
+
for i in range(total_frames):
|
|
129
|
+
t = t_frame + i * blocksize / sample_rate
|
|
130
|
+
signal = amplitude * np.sin(2 * np.pi * freq * t)
|
|
131
|
+
signal_int16 = np.int16(signal * 32767)
|
|
132
|
+
frame = AudioFrame(
|
|
133
|
+
signal_int16.tobytes(),
|
|
134
|
+
sample_rate,
|
|
135
|
+
1,
|
|
136
|
+
blocksize,
|
|
137
|
+
)
|
|
138
|
+
yield frame
|
livekit/rtc/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.1"
|