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/__init__.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
|
+
"""LiveKit SDK for Python
|
|
16
|
+
`pip install livekit`
|
|
17
|
+
|
|
18
|
+
See https://docs.livekit.io/home/client/connect/#installing-the-livekit-sdk for more information.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ._proto import stats_pb2 as stats
|
|
22
|
+
from ._proto.e2ee_pb2 import EncryptionState, EncryptionType, KeyDerivationFunction
|
|
23
|
+
from ._proto.participant_pb2 import (
|
|
24
|
+
ParticipantKind,
|
|
25
|
+
ParticipantKindDetail,
|
|
26
|
+
ParticipantState,
|
|
27
|
+
DisconnectReason,
|
|
28
|
+
)
|
|
29
|
+
from ._proto.room_pb2 import (
|
|
30
|
+
ConnectionState,
|
|
31
|
+
ContinualGatheringPolicy,
|
|
32
|
+
DataPacketKind,
|
|
33
|
+
IceServer,
|
|
34
|
+
IceTransportType,
|
|
35
|
+
SimulateScenarioKind,
|
|
36
|
+
TrackPublishOptions,
|
|
37
|
+
VideoEncoding,
|
|
38
|
+
VideoEncoderBackend,
|
|
39
|
+
)
|
|
40
|
+
from ._proto.track_pb2 import (
|
|
41
|
+
FrameMetadataFeature,
|
|
42
|
+
StreamState,
|
|
43
|
+
TrackKind,
|
|
44
|
+
TrackSource,
|
|
45
|
+
ParticipantTrackPermission,
|
|
46
|
+
)
|
|
47
|
+
from ._proto.video_frame_pb2 import (
|
|
48
|
+
EncodedFrameType,
|
|
49
|
+
FrameMetadata,
|
|
50
|
+
VideoBufferType,
|
|
51
|
+
VideoCodec,
|
|
52
|
+
VideoRotation,
|
|
53
|
+
)
|
|
54
|
+
from ._proto.track_publication_pb2 import VideoQuality
|
|
55
|
+
from .audio_frame import AudioFrame
|
|
56
|
+
from .audio_source import AudioSource
|
|
57
|
+
from .audio_stream import AudioFrameEvent, AudioStream, NoiseCancellationOptions
|
|
58
|
+
from .audio_filter import AudioFilter
|
|
59
|
+
from .e2ee import (
|
|
60
|
+
E2EEManager,
|
|
61
|
+
E2EEOptions,
|
|
62
|
+
FrameCryptor,
|
|
63
|
+
KeyProvider,
|
|
64
|
+
KeyProviderOptions,
|
|
65
|
+
)
|
|
66
|
+
from .participant import (
|
|
67
|
+
ConnectionQuality,
|
|
68
|
+
DegradationPreference,
|
|
69
|
+
LocalParticipant,
|
|
70
|
+
Participant,
|
|
71
|
+
RemoteParticipant,
|
|
72
|
+
)
|
|
73
|
+
from .room import (
|
|
74
|
+
ConnectError,
|
|
75
|
+
DataPacket,
|
|
76
|
+
DataStreamOptions,
|
|
77
|
+
Room,
|
|
78
|
+
RoomOptions,
|
|
79
|
+
RtcConfiguration,
|
|
80
|
+
SipDTMF,
|
|
81
|
+
RtcStats,
|
|
82
|
+
)
|
|
83
|
+
from .track import (
|
|
84
|
+
AudioTrack,
|
|
85
|
+
LocalAudioTrack,
|
|
86
|
+
LocalTrack,
|
|
87
|
+
LocalVideoTrack,
|
|
88
|
+
RemoteAudioTrack,
|
|
89
|
+
RemoteTrack,
|
|
90
|
+
RemoteVideoTrack,
|
|
91
|
+
Track,
|
|
92
|
+
VideoTrack,
|
|
93
|
+
)
|
|
94
|
+
from .event_emitter import EventEmitter
|
|
95
|
+
from .track_publication import (
|
|
96
|
+
LocalTrackPublication,
|
|
97
|
+
RemoteTrackPublication,
|
|
98
|
+
TrackPublication,
|
|
99
|
+
)
|
|
100
|
+
from .transcription import Transcription, TranscriptionSegment
|
|
101
|
+
from .version import __version__
|
|
102
|
+
from .video_frame import (
|
|
103
|
+
VideoFrame,
|
|
104
|
+
)
|
|
105
|
+
from .encoded_video import (
|
|
106
|
+
EncodedVideoFrame,
|
|
107
|
+
EncodedVideoSource,
|
|
108
|
+
EncodedVideoSourceFeedback,
|
|
109
|
+
EncodedRateControl,
|
|
110
|
+
)
|
|
111
|
+
from .video_source import VideoSource
|
|
112
|
+
from .video_stream import VideoFrameEvent, VideoStream
|
|
113
|
+
from .audio_resampler import AudioResampler, AudioResamplerQuality
|
|
114
|
+
from .audio_mixer import AudioMixer
|
|
115
|
+
from .apm import AudioProcessingModule
|
|
116
|
+
from .platform_audio import (
|
|
117
|
+
PlatformAudio,
|
|
118
|
+
PlatformAudioSource,
|
|
119
|
+
PlatformAudioOptions,
|
|
120
|
+
PlatformAudioError,
|
|
121
|
+
AudioDeviceInfo,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
from .media_devices import MediaDevices as MediaDevices
|
|
126
|
+
|
|
127
|
+
_HAS_MEDIA_DEVICES = True
|
|
128
|
+
except Exception: # pragma: no cover - optional dependency (sounddevice)
|
|
129
|
+
_HAS_MEDIA_DEVICES = False
|
|
130
|
+
from .utils import combine_audio_frames
|
|
131
|
+
from .rpc import RpcCallInfo, RpcError, RpcInterceptor, RpcInvocationData
|
|
132
|
+
from .synchronizer import AVSynchronizer
|
|
133
|
+
from .data_stream import (
|
|
134
|
+
TextStreamInfo,
|
|
135
|
+
ByteStreamInfo,
|
|
136
|
+
TextStreamReader,
|
|
137
|
+
TextStreamWriter,
|
|
138
|
+
ByteStreamWriter,
|
|
139
|
+
ByteStreamReader,
|
|
140
|
+
StreamError,
|
|
141
|
+
)
|
|
142
|
+
from .data_track import (
|
|
143
|
+
LocalDataTrack,
|
|
144
|
+
RemoteDataTrack,
|
|
145
|
+
DataTrackStream,
|
|
146
|
+
DataTrackFrame,
|
|
147
|
+
DataTrackInfo,
|
|
148
|
+
PushFrameError,
|
|
149
|
+
SubscribeDataTrackError,
|
|
150
|
+
)
|
|
151
|
+
from .frame_processor import FrameProcessor
|
|
152
|
+
|
|
153
|
+
__all__ = [
|
|
154
|
+
"EncodedFrameType",
|
|
155
|
+
"EncodedVideoFrame",
|
|
156
|
+
"EncodedVideoSource",
|
|
157
|
+
"EncodedVideoSourceFeedback",
|
|
158
|
+
"EncodedRateControl",
|
|
159
|
+
"VideoEncoderBackend",
|
|
160
|
+
"ConnectionQuality",
|
|
161
|
+
"ConnectionState",
|
|
162
|
+
"DataPacketKind",
|
|
163
|
+
"DegradationPreference",
|
|
164
|
+
"TrackPublishOptions",
|
|
165
|
+
"IceTransportType",
|
|
166
|
+
"ContinualGatheringPolicy",
|
|
167
|
+
"IceServer",
|
|
168
|
+
"EncryptionType",
|
|
169
|
+
"EncryptionState",
|
|
170
|
+
"KeyDerivationFunction",
|
|
171
|
+
"StreamState",
|
|
172
|
+
"TrackKind",
|
|
173
|
+
"TrackSource",
|
|
174
|
+
"FrameMetadataFeature",
|
|
175
|
+
"ParticipantTrackPermission",
|
|
176
|
+
"VideoBufferType",
|
|
177
|
+
"VideoRotation",
|
|
178
|
+
"FrameMetadata",
|
|
179
|
+
"stats",
|
|
180
|
+
"AudioFrame",
|
|
181
|
+
"AudioSource",
|
|
182
|
+
"AudioStream",
|
|
183
|
+
"NoiseCancellationOptions",
|
|
184
|
+
"AudioFilter",
|
|
185
|
+
"AudioFrameEvent",
|
|
186
|
+
"LocalParticipant",
|
|
187
|
+
"Participant",
|
|
188
|
+
"ParticipantKind",
|
|
189
|
+
"ParticipantKindDetail",
|
|
190
|
+
"ParticipantState",
|
|
191
|
+
"DisconnectReason",
|
|
192
|
+
"RemoteParticipant",
|
|
193
|
+
"ConnectError",
|
|
194
|
+
"Room",
|
|
195
|
+
"RoomOptions",
|
|
196
|
+
"DataStreamOptions",
|
|
197
|
+
"RtcConfiguration",
|
|
198
|
+
"SimulateScenarioKind",
|
|
199
|
+
"SipDTMF",
|
|
200
|
+
"RtcStats",
|
|
201
|
+
"DataPacket",
|
|
202
|
+
"LocalAudioTrack",
|
|
203
|
+
"LocalVideoTrack",
|
|
204
|
+
"RemoteAudioTrack",
|
|
205
|
+
"RemoteVideoTrack",
|
|
206
|
+
"Track",
|
|
207
|
+
"LocalTrack",
|
|
208
|
+
"RemoteTrack",
|
|
209
|
+
"AudioTrack",
|
|
210
|
+
"VideoTrack",
|
|
211
|
+
"E2EEManager",
|
|
212
|
+
"E2EEOptions",
|
|
213
|
+
"KeyProviderOptions",
|
|
214
|
+
"KeyProvider",
|
|
215
|
+
"FrameCryptor",
|
|
216
|
+
"LocalTrackPublication",
|
|
217
|
+
"RemoteTrackPublication",
|
|
218
|
+
"TrackPublication",
|
|
219
|
+
"Transcription",
|
|
220
|
+
"TranscriptionSegment",
|
|
221
|
+
"VideoCodec",
|
|
222
|
+
"VideoEncoding",
|
|
223
|
+
"VideoQuality",
|
|
224
|
+
"VideoFrame",
|
|
225
|
+
"VideoFrameEvent",
|
|
226
|
+
"VideoSource",
|
|
227
|
+
"VideoStream",
|
|
228
|
+
"AudioMixer",
|
|
229
|
+
"AudioResampler",
|
|
230
|
+
"AudioResamplerQuality",
|
|
231
|
+
"RpcError",
|
|
232
|
+
"RpcInvocationData",
|
|
233
|
+
"RpcCallInfo",
|
|
234
|
+
"RpcInterceptor",
|
|
235
|
+
"EventEmitter",
|
|
236
|
+
"combine_audio_frames",
|
|
237
|
+
"AVSynchronizer",
|
|
238
|
+
"TextStreamInfo",
|
|
239
|
+
"ByteStreamInfo",
|
|
240
|
+
"TextStreamReader",
|
|
241
|
+
"TextStreamWriter",
|
|
242
|
+
"ByteStreamReader",
|
|
243
|
+
"ByteStreamWriter",
|
|
244
|
+
"StreamError",
|
|
245
|
+
"AudioProcessingModule",
|
|
246
|
+
"PlatformAudio",
|
|
247
|
+
"PlatformAudioSource",
|
|
248
|
+
"PlatformAudioOptions",
|
|
249
|
+
"PlatformAudioError",
|
|
250
|
+
"AudioDeviceInfo",
|
|
251
|
+
"FrameProcessor",
|
|
252
|
+
"LocalDataTrack",
|
|
253
|
+
"RemoteDataTrack",
|
|
254
|
+
"DataTrackStream",
|
|
255
|
+
"DataTrackFrame",
|
|
256
|
+
"DataTrackInfo",
|
|
257
|
+
"PushFrameError",
|
|
258
|
+
"SubscribeDataTrackError",
|
|
259
|
+
"__version__",
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
# add MediaDevices if available
|
|
263
|
+
if _HAS_MEDIA_DEVICES:
|
|
264
|
+
__all__.append("MediaDevices")
|
|
@@ -0,0 +1,321 @@
|
|
|
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 signal
|
|
18
|
+
import asyncio
|
|
19
|
+
import sys
|
|
20
|
+
from contextlib import ExitStack
|
|
21
|
+
import ctypes
|
|
22
|
+
import importlib.resources
|
|
23
|
+
from .version import __version__
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import platform
|
|
27
|
+
import atexit
|
|
28
|
+
import threading
|
|
29
|
+
import weakref
|
|
30
|
+
from typing import Callable, Generic, List, Optional, TypeVar
|
|
31
|
+
|
|
32
|
+
from ._proto import ffi_pb2 as proto_ffi
|
|
33
|
+
from ._utils import Queue, classproperty
|
|
34
|
+
from .log import logger
|
|
35
|
+
|
|
36
|
+
_resource_files = ExitStack()
|
|
37
|
+
atexit.register(_resource_files.close)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _lib_name() -> Optional[str]:
|
|
41
|
+
if platform.system() == "Linux":
|
|
42
|
+
return "liblivekit_ffi.so"
|
|
43
|
+
elif platform.system() == "Darwin":
|
|
44
|
+
return "liblivekit_ffi.dylib"
|
|
45
|
+
elif platform.system() == "Windows":
|
|
46
|
+
return "livekit_ffi.dll"
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_ffi_lib() -> ctypes.CDLL:
|
|
51
|
+
# allow to override the lib path using an env var
|
|
52
|
+
libpath = os.environ.get("LIVEKIT_LIB_PATH", "").strip()
|
|
53
|
+
if libpath:
|
|
54
|
+
return ctypes.CDLL(libpath)
|
|
55
|
+
|
|
56
|
+
libname = _lib_name()
|
|
57
|
+
if libname is None:
|
|
58
|
+
raise Exception(
|
|
59
|
+
f"no ffi library found for platform {platform.system()}. "
|
|
60
|
+
"Set LIVEKIT_LIB_PATH to specify the lib path"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
res = importlib.resources.files("livekit.rtc.resources") / libname
|
|
64
|
+
ctx = importlib.resources.as_file(res)
|
|
65
|
+
path = _resource_files.enter_context(ctx)
|
|
66
|
+
return ctypes.CDLL(str(path))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
ffi_cb_fnc = ctypes.CFUNCTYPE(None, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t)
|
|
70
|
+
|
|
71
|
+
INVALID_HANDLE = 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_live_handles: weakref.WeakSet[FfiHandle] = weakref.WeakSet()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class FfiHandle:
|
|
78
|
+
def __init__(self, handle: int) -> None:
|
|
79
|
+
self.handle = handle
|
|
80
|
+
self._disposed = False
|
|
81
|
+
_live_handles.add(self)
|
|
82
|
+
|
|
83
|
+
def __del__(self) -> None:
|
|
84
|
+
self.dispose()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def disposed(self) -> bool:
|
|
88
|
+
return self._disposed
|
|
89
|
+
|
|
90
|
+
def mark_consumed(self) -> None:
|
|
91
|
+
"""Marks the handle as already released on the native side.
|
|
92
|
+
|
|
93
|
+
Some requests take ownership of their handle (``take_handle``) rather
|
|
94
|
+
than borrowing it. After issuing one, the handle must not be dropped
|
|
95
|
+
again, so this suppresses the drop that dispose/GC would otherwise do.
|
|
96
|
+
"""
|
|
97
|
+
self._disposed = True
|
|
98
|
+
|
|
99
|
+
def dispose(self) -> None:
|
|
100
|
+
if self.handle != INVALID_HANDLE and not self._disposed:
|
|
101
|
+
self._disposed = True
|
|
102
|
+
ffi = FfiClient._instance
|
|
103
|
+
if ffi is None or ffi._pid != os.getpid() or getattr(FfiClient, "_ffi_disposed", False):
|
|
104
|
+
return
|
|
105
|
+
dropped = ffi._ffi_lib.livekit_ffi_drop_handle(ctypes.c_uint64(self.handle))
|
|
106
|
+
assert dropped
|
|
107
|
+
|
|
108
|
+
def __repr__(self) -> str:
|
|
109
|
+
return f"FfiHandle({self.handle})"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
T = TypeVar("T")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class FfiQueue(Generic[T]):
|
|
116
|
+
def __init__(self) -> None:
|
|
117
|
+
self._lock = threading.RLock()
|
|
118
|
+
# Format: (queue, loop, filter_fn or None)
|
|
119
|
+
self._subscribers: List[
|
|
120
|
+
tuple[Queue[T], asyncio.AbstractEventLoop, Optional[Callable[[T], bool]]]
|
|
121
|
+
] = []
|
|
122
|
+
|
|
123
|
+
def put(self, item: T) -> None:
|
|
124
|
+
with self._lock:
|
|
125
|
+
for queue, loop, filter_fn in self._subscribers:
|
|
126
|
+
# If filter provided, skip items that don't match
|
|
127
|
+
if filter_fn is not None:
|
|
128
|
+
try:
|
|
129
|
+
if not filter_fn(item):
|
|
130
|
+
continue
|
|
131
|
+
except Exception:
|
|
132
|
+
pass # On filter error, deliver the item
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
loop.call_soon_threadsafe(queue.put_nowait, item)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
# this could happen if user closes the runloop without unsubscribing first
|
|
138
|
+
# it's not good when it does occur, but we should not fail the entire runloop
|
|
139
|
+
logger.error("error putting to queue: %s", e)
|
|
140
|
+
|
|
141
|
+
def subscribe(
|
|
142
|
+
self,
|
|
143
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
144
|
+
filter_fn: Optional[Callable[[T], bool]] = None,
|
|
145
|
+
) -> Queue[T]:
|
|
146
|
+
"""Subscribe to FFI events.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
loop: Event loop to use (defaults to current).
|
|
150
|
+
filter_fn: Optional filter function. If provided, only items where
|
|
151
|
+
filter_fn(item) returns True will be delivered.
|
|
152
|
+
If None, receives all events (original behavior).
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Queue to receive events from.
|
|
156
|
+
"""
|
|
157
|
+
with self._lock:
|
|
158
|
+
queue = Queue[T]()
|
|
159
|
+
loop = loop or asyncio.get_event_loop()
|
|
160
|
+
self._subscribers.append((queue, loop, filter_fn))
|
|
161
|
+
return queue
|
|
162
|
+
|
|
163
|
+
def unsubscribe(self, queue: Queue[T]) -> None:
|
|
164
|
+
with self._lock:
|
|
165
|
+
# looping here is ok, since we don't expect a lot of subscribers
|
|
166
|
+
for i, (q, _, _) in enumerate(self._subscribers):
|
|
167
|
+
if q == queue:
|
|
168
|
+
self._subscribers.pop(i)
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@ctypes.CFUNCTYPE(None, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) # type: ignore[untyped-decorator]
|
|
173
|
+
def ffi_event_callback(
|
|
174
|
+
data_ptr: ctypes.POINTER(ctypes.c_uint8), # type: ignore
|
|
175
|
+
data_len: ctypes.c_size_t,
|
|
176
|
+
) -> None:
|
|
177
|
+
event_data = ctypes.string_at(data_ptr, int(data_len))
|
|
178
|
+
event = proto_ffi.FfiEvent()
|
|
179
|
+
event.ParseFromString(event_data)
|
|
180
|
+
|
|
181
|
+
which = event.WhichOneof("message")
|
|
182
|
+
if which == "logs":
|
|
183
|
+
for record in event.logs.records:
|
|
184
|
+
level = to_python_level(record.level)
|
|
185
|
+
debug_env = os.environ.get("LIVEKIT_RTC_DEBUG", "").strip().lower()
|
|
186
|
+
rtc_debug = debug_env in ("true", "1")
|
|
187
|
+
|
|
188
|
+
if level == logging.DEBUG and not rtc_debug:
|
|
189
|
+
# ignore the rtc debug logs by default
|
|
190
|
+
if record.target == "libwebrtc" or record.target.startswith("livekit"):
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
if level is not None:
|
|
194
|
+
logger.log(
|
|
195
|
+
level,
|
|
196
|
+
"%s:%s:%s - %s",
|
|
197
|
+
record.target,
|
|
198
|
+
record.line,
|
|
199
|
+
record.module_path,
|
|
200
|
+
record.message,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return # no need to queue the logs
|
|
204
|
+
elif which == "panic":
|
|
205
|
+
print("FFI Panic: ", event.panic.message, file=sys.stderr, flush=True)
|
|
206
|
+
# We are in a unrecoverable state, terminate the process
|
|
207
|
+
os.kill(os.getpid(), signal.SIGTERM)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
FfiClient.instance.queue.put(event)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def to_python_level(level: proto_ffi.LogLevel.ValueType) -> Optional[int]:
|
|
214
|
+
if level == proto_ffi.LogLevel.LOG_ERROR:
|
|
215
|
+
return logging.ERROR
|
|
216
|
+
elif level == proto_ffi.LogLevel.LOG_WARN:
|
|
217
|
+
return logging.WARN
|
|
218
|
+
elif level == proto_ffi.LogLevel.LOG_INFO:
|
|
219
|
+
return logging.INFO
|
|
220
|
+
elif level == proto_ffi.LogLevel.LOG_DEBUG:
|
|
221
|
+
return logging.DEBUG
|
|
222
|
+
elif level == proto_ffi.LogLevel.LOG_TRACE:
|
|
223
|
+
# Don't show TRACE logs inside DEBUG, it is too verbos
|
|
224
|
+
# Python's logging doesn't have a TRACE level
|
|
225
|
+
# return logging.DEBUG
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class FfiClient:
|
|
232
|
+
_instance: Optional["FfiClient"] = None
|
|
233
|
+
_ffi_disposed: bool = False
|
|
234
|
+
_instance_lock = threading.Lock()
|
|
235
|
+
|
|
236
|
+
@classproperty
|
|
237
|
+
def instance(cls) -> "FfiClient":
|
|
238
|
+
if cls._instance is not None:
|
|
239
|
+
return cls._instance
|
|
240
|
+
# threads that race here would each get their own client and lose events sent to the other
|
|
241
|
+
with cls._instance_lock:
|
|
242
|
+
if cls._instance is None:
|
|
243
|
+
cls._instance = FfiClient()
|
|
244
|
+
return cls._instance
|
|
245
|
+
|
|
246
|
+
def __init__(self) -> None:
|
|
247
|
+
FfiClient._ffi_disposed = False
|
|
248
|
+
self._pid = os.getpid()
|
|
249
|
+
self._lock = threading.RLock()
|
|
250
|
+
self._queue = FfiQueue[proto_ffi.FfiEvent]()
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
self._ffi_lib = get_ffi_lib()
|
|
254
|
+
except Exception as e:
|
|
255
|
+
libname = _lib_name() or "livekit_ffi"
|
|
256
|
+
raise ImportError(
|
|
257
|
+
"failed to load %s: %s\n"
|
|
258
|
+
"Install the livekit package with: pip install livekit\n"
|
|
259
|
+
"Or set LIVEKIT_LIB_PATH to the path of the native library." % (libname, e)
|
|
260
|
+
) from None
|
|
261
|
+
self._ffi_lib.livekit_ffi_initialize.argtypes = [
|
|
262
|
+
ffi_cb_fnc,
|
|
263
|
+
ctypes.c_bool,
|
|
264
|
+
ctypes.c_char_p,
|
|
265
|
+
ctypes.c_char_p,
|
|
266
|
+
]
|
|
267
|
+
self._ffi_lib.livekit_ffi_request.argtypes = [
|
|
268
|
+
ctypes.POINTER(ctypes.c_ubyte),
|
|
269
|
+
ctypes.c_size_t,
|
|
270
|
+
ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte)),
|
|
271
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
272
|
+
]
|
|
273
|
+
self._ffi_lib.livekit_ffi_request.restype = ctypes.c_uint64
|
|
274
|
+
self._ffi_lib.livekit_ffi_drop_handle.argtypes = [ctypes.c_uint64]
|
|
275
|
+
self._ffi_lib.livekit_ffi_drop_handle.restype = ctypes.c_bool
|
|
276
|
+
self._ffi_lib.livekit_ffi_dispose.argtypes = []
|
|
277
|
+
self._ffi_lib.livekit_ffi_dispose.restype = None
|
|
278
|
+
|
|
279
|
+
self._ffi_lib.livekit_ffi_initialize(
|
|
280
|
+
ffi_event_callback, True, b"python", __version__.encode("ascii")
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
@atexit.register
|
|
284
|
+
def _dispose_lk_ffi() -> None:
|
|
285
|
+
self._dispose()
|
|
286
|
+
|
|
287
|
+
def _dispose(self) -> None:
|
|
288
|
+
if self._pid != os.getpid() or FfiClient._ffi_disposed:
|
|
289
|
+
return
|
|
290
|
+
FfiClient._ffi_disposed = True
|
|
291
|
+
for handle in list(_live_handles):
|
|
292
|
+
handle.mark_consumed()
|
|
293
|
+
self._ffi_lib.livekit_ffi_dispose()
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def queue(self) -> FfiQueue[proto_ffi.FfiEvent]:
|
|
297
|
+
return self._queue
|
|
298
|
+
|
|
299
|
+
def request(self, req: proto_ffi.FfiRequest) -> proto_ffi.FfiResponse:
|
|
300
|
+
if self._pid != os.getpid():
|
|
301
|
+
raise RuntimeError(
|
|
302
|
+
"livekit.rtc was used in a parent process before fork(); the native "
|
|
303
|
+
"runtime cannot be used across fork(). Do not create or connect a Room "
|
|
304
|
+
"(or use any livekit.rtc object) before forking child processes."
|
|
305
|
+
)
|
|
306
|
+
proto_data = req.SerializeToString()
|
|
307
|
+
proto_len = len(proto_data)
|
|
308
|
+
data = ctypes.cast(ctypes.c_char_p(proto_data), ctypes.POINTER(ctypes.c_ubyte))
|
|
309
|
+
|
|
310
|
+
resp_ptr = ctypes.POINTER(ctypes.c_ubyte)()
|
|
311
|
+
resp_len = ctypes.c_size_t()
|
|
312
|
+
handle = self._ffi_lib.livekit_ffi_request(
|
|
313
|
+
data, proto_len, ctypes.byref(resp_ptr), ctypes.byref(resp_len)
|
|
314
|
+
)
|
|
315
|
+
assert handle != INVALID_HANDLE
|
|
316
|
+
|
|
317
|
+
resp_data = ctypes.string_at(resp_ptr, resp_len.value)
|
|
318
|
+
resp = proto_ffi.FfiResponse()
|
|
319
|
+
resp.ParseFromString(resp_data)
|
|
320
|
+
FfiHandle(handle).dispose()
|
|
321
|
+
return resp
|
|
File without changes
|