livekit 0.7.0.dev1__tar.gz → 0.7.1.dev1__tar.gz

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.
Files changed (48) hide show
  1. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/PKG-INFO +3 -3
  2. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/__init__.py +63 -2
  3. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_ffi_client.py +23 -14
  4. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_utils.py +21 -0
  5. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/audio_frame.py +3 -3
  6. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/audio_source.py +5 -5
  7. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/audio_stream.py +7 -6
  8. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/e2ee.py +11 -11
  9. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/participant.py +13 -12
  10. livekit-0.7.1.dev1/livekit/rtc/resources/__init__.py +1 -0
  11. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/room.py +27 -15
  12. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/track.py +6 -6
  13. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/track_publication.py +2 -2
  14. livekit-0.7.1.dev1/livekit/rtc/version.py +1 -0
  15. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/video_frame.py +5 -5
  16. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/video_source.py +3 -3
  17. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/video_stream.py +7 -6
  18. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit.egg-info/PKG-INFO +3 -3
  19. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit.egg-info/SOURCES.txt +2 -1
  20. livekit-0.7.1.dev1/livekit.egg-info/requires.txt +2 -0
  21. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/pyproject.toml +2 -0
  22. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/setup.py +3 -29
  23. livekit-0.7.0.dev1/livekit/rtc/version.py +0 -1
  24. livekit-0.7.0.dev1/livekit.egg-info/requires.txt +0 -2
  25. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/README.md +0 -0
  26. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_event_emitter.py +0 -0
  27. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/__init__.py +0 -0
  28. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/audio_frame_pb2.py +0 -0
  29. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/audio_frame_pb2.pyi +0 -0
  30. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/e2ee_pb2.py +0 -0
  31. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/e2ee_pb2.pyi +0 -0
  32. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/ffi_pb2.py +0 -0
  33. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/ffi_pb2.pyi +0 -0
  34. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/handle_pb2.py +0 -0
  35. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/handle_pb2.pyi +0 -0
  36. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/participant_pb2.py +0 -0
  37. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/participant_pb2.pyi +0 -0
  38. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/room_pb2.py +0 -0
  39. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/room_pb2.pyi +0 -0
  40. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/stats_pb2.py +0 -0
  41. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/stats_pb2.pyi +0 -0
  42. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/track_pb2.py +0 -0
  43. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/track_pb2.pyi +0 -0
  44. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/video_frame_pb2.py +0 -0
  45. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit/rtc/_proto/video_frame_pb2.pyi +0 -0
  46. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit.egg-info/dependency_links.txt +0 -0
  47. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/livekit.egg-info/top_level.txt +0 -0
  48. {livekit-0.7.0.dev1 → livekit-0.7.1.dev1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: livekit
3
- Version: 0.7.0.dev1
3
+ Version: 0.7.1.dev1
4
4
  Summary: Python Real-time SDK for LiveKit
5
5
  Home-page: https://github.com/livekit/python-sdks
6
6
  License: Apache-2.0
@@ -19,8 +19,8 @@ Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3 :: Only
20
20
  Requires-Python: >=3.9.0
21
21
  Description-Content-Type: text/markdown
22
- Requires-Dist: protobuf>=3.1.0
23
- Requires-Dist: types-protobuf>=3.1.0
22
+ Requires-Dist: protobuf>=4
23
+ Requires-Dist: types-protobuf>=4
24
24
 
25
25
  # LiveKit Real-time Python SDK
26
26
 
@@ -15,7 +15,6 @@
15
15
  """LiveKit RTC SDK
16
16
  """
17
17
 
18
- # flake8: noqa
19
18
  from ._proto.room_pb2 import (
20
19
  ConnectionQuality,
21
20
  ConnectionState,
@@ -33,7 +32,7 @@ from .audio_frame import AudioFrame
33
32
  from .audio_source import AudioSource
34
33
  from .audio_stream import AudioStream
35
34
  from .participant import LocalParticipant, Participant, RemoteParticipant
36
- from .room import ConnectError, Room, RoomOptions, RtcConfiguration
35
+ from .room import ConnectError, Room, RoomOptions, RtcConfiguration, DataPacket
37
36
  from .track import (
38
37
  LocalAudioTrack,
39
38
  LocalVideoTrack,
@@ -75,3 +74,65 @@ from .video_source import VideoSource
75
74
  from .video_stream import VideoStream
76
75
 
77
76
  from .version import __version__
77
+
78
+ __all__ = [
79
+ "ConnectionQuality",
80
+ "ConnectionState",
81
+ "DataPacketKind",
82
+ "TrackPublishOptions",
83
+ "IceTransportType",
84
+ "ContinualGatheringPolicy",
85
+ "IceServer",
86
+ "EncryptionType",
87
+ "EncryptionState",
88
+ "StreamState",
89
+ "TrackKind",
90
+ "TrackSource",
91
+ "VideoFormatType",
92
+ "VideoFrameBufferType",
93
+ "VideoRotation",
94
+ "stats",
95
+ "AudioFrame",
96
+ "AudioSource",
97
+ "AudioStream",
98
+ "LocalParticipant",
99
+ "Participant",
100
+ "RemoteParticipant",
101
+ "ConnectError",
102
+ "Room",
103
+ "RoomOptions",
104
+ "RtcConfiguration",
105
+ "DataPacket",
106
+ "LocalAudioTrack",
107
+ "LocalVideoTrack",
108
+ "RemoteAudioTrack",
109
+ "RemoteVideoTrack",
110
+ "Track",
111
+ "LocalTrack",
112
+ "RemoteTrack",
113
+ "AudioTrack",
114
+ "VideoTrack",
115
+ "E2EEManager",
116
+ "E2EEOptions",
117
+ "KeyProviderOptions",
118
+ "KeyProvider",
119
+ "FrameCryptor",
120
+ "LocalTrackPublication",
121
+ "RemoteTrackPublication",
122
+ "TrackPublication",
123
+ "ArgbFrame",
124
+ "I010Buffer",
125
+ "I420ABuffer",
126
+ "I420Buffer",
127
+ "I422Buffer",
128
+ "NativeVideoBuffer",
129
+ "NV12Buffer",
130
+ "PlanarYuv8Buffer",
131
+ "PlanarYuv16Buffer",
132
+ "PlanarYuvBuffer",
133
+ "VideoFrame",
134
+ "VideoFrameBuffer",
135
+ "VideoSource",
136
+ "VideoStream",
137
+ "__version__",
138
+ ]
@@ -13,22 +13,26 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import asyncio
16
- import logging
16
+ from contextlib import ExitStack
17
17
  import ctypes
18
+ import importlib.resources
19
+ import logging
18
20
  import os
19
21
  import platform
22
+ import atexit
20
23
  import threading
21
24
  from typing import Generic, List, Optional, TypeVar
22
25
 
23
- import pkg_resources
24
-
25
26
  from ._proto import ffi_pb2 as proto_ffi
26
- from ._utils import Queue
27
+ from ._utils import Queue, classproperty
27
28
 
28
29
  logger = logging.getLogger("livekit")
29
30
 
31
+ _resource_files = ExitStack()
32
+ atexit.register(_resource_files.close)
33
+
30
34
 
31
- def get_ffi_lib_path():
35
+ def get_ffi_lib():
32
36
  # allow to override the lib path using an env var
33
37
  libpath = os.environ.get("LIVEKIT_LIB_PATH", "").strip()
34
38
  if libpath:
@@ -46,13 +50,13 @@ def get_ffi_lib_path():
46
50
  Set LIVEKIT_LIB_PATH to specify a the lib path"
47
51
  )
48
52
 
49
- libpath = pkg_resources.resource_filename(
50
- "livekit.rtc", os.path.join("resources", libname)
51
- )
52
- return libpath
53
+ res = importlib.resources.files("livekit.rtc.resources") / libname
54
+ ctx = importlib.resources.as_file(res)
55
+ path = _resource_files.enter_context(ctx)
56
+ return ctypes.CDLL(str(path))
53
57
 
54
58
 
55
- ffi_lib = ctypes.CDLL(get_ffi_lib_path())
59
+ ffi_lib = get_ffi_lib()
56
60
  ffi_cb_fnc = ctypes.CFUNCTYPE(None, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t)
57
61
 
58
62
  # C function types
@@ -142,7 +146,7 @@ def ffi_event_callback(
142
146
 
143
147
  return # no need to queue the logs
144
148
 
145
- ffi_client.queue.put(event)
149
+ FfiClient.instance.queue.put(event)
146
150
 
147
151
 
148
152
  def to_python_level(level: proto_ffi.LogLevel.ValueType) -> int:
@@ -161,6 +165,14 @@ def to_python_level(level: proto_ffi.LogLevel.ValueType) -> int:
161
165
 
162
166
 
163
167
  class FfiClient:
168
+ _instance: Optional["FfiClient"] = None
169
+
170
+ @classproperty
171
+ def instance(self):
172
+ if self._instance is None:
173
+ self._instance = FfiClient()
174
+ return self._instance
175
+
164
176
  def __init__(self) -> None:
165
177
  self._lock = threading.RLock()
166
178
  self._queue = FfiQueue[proto_ffi.FfiEvent]()
@@ -188,6 +200,3 @@ class FfiClient:
188
200
 
189
201
  FfiHandle(handle)
190
202
  return resp
191
-
192
-
193
- ffi_client = FfiClient()
@@ -1,8 +1,29 @@
1
1
  import asyncio
2
+ import logging
2
3
  from collections import deque
3
4
  import ctypes
4
5
  from typing import Callable, Generic, List, TypeVar
5
6
 
7
+ logger = logging.getLogger("livekit")
8
+
9
+
10
+ class classproperty(object):
11
+ def __init__(self, f):
12
+ self.f = classmethod(f)
13
+
14
+ def __get__(self, *a):
15
+ return self.f.__get__(*a)()
16
+
17
+
18
+ def task_done_logger(task: asyncio.Task) -> None:
19
+ if task.cancelled():
20
+ logger.info("task cancelled: %s", task)
21
+ return
22
+
23
+ if task.exception():
24
+ logger.error("task exception: %s", task, exc_info=task.exception())
25
+ return
26
+
6
27
 
7
28
  def get_address(data: memoryview) -> int:
8
29
  """Get the address of a buffer using ctypes"""
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import ctypes
16
- from ._ffi_client import FfiHandle, ffi_client
16
+ from ._ffi_client import FfiHandle, FfiClient
17
17
  from ._proto import audio_frame_pb2 as proto_audio
18
18
  from ._proto import ffi_pb2 as proto_ffi
19
19
  from ._utils import get_address
@@ -65,7 +65,7 @@ class AudioFrame:
65
65
  req = proto_ffi.FfiRequest()
66
66
  req.new_audio_resampler.CopyFrom(proto_audio.NewAudioResamplerRequest())
67
67
 
68
- resp = ffi_client.request(req)
68
+ resp = FfiClient.instance.request(req)
69
69
  resampler_handle = FfiHandle(resp.new_audio_resampler.resampler.handle.id)
70
70
 
71
71
  resample_req = proto_ffi.FfiRequest()
@@ -74,7 +74,7 @@ class AudioFrame:
74
74
  resample_req.remix_and_resample.sample_rate = sample_rate
75
75
  resample_req.remix_and_resample.num_channels = num_channels
76
76
 
77
- resp = ffi_client.request(resample_req)
77
+ resp = FfiClient.instance.request(resample_req)
78
78
  return AudioFrame._from_owned_info(resp.remix_and_resample.buffer)
79
79
 
80
80
  def _proto_info(self) -> proto_audio.AudioFrameBufferInfo:
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from ._ffi_client import FfiHandle, ffi_client
15
+ from ._ffi_client import FfiHandle, FfiClient
16
16
  from ._proto import audio_frame_pb2 as proto_audio_frame
17
17
  from ._proto import ffi_pb2 as proto_ffi
18
18
  from .audio_frame import AudioFrame
@@ -27,7 +27,7 @@ class AudioSource:
27
27
  req.new_audio_source.sample_rate = sample_rate
28
28
  req.new_audio_source.num_channels = num_channels
29
29
 
30
- resp = ffi_client.request(req)
30
+ resp = FfiClient.instance.request(req)
31
31
  self._info = resp.new_audio_source.source
32
32
  self._ffi_handle = FfiHandle(self._info.handle.id)
33
33
 
@@ -37,15 +37,15 @@ class AudioSource:
37
37
  req.capture_audio_frame.source_handle = self._ffi_handle.handle
38
38
  req.capture_audio_frame.buffer.CopyFrom(frame._proto_info())
39
39
 
40
- queue = ffi_client.queue.subscribe()
40
+ queue = FfiClient.instance.queue.subscribe()
41
41
  try:
42
- resp = ffi_client.request(req)
42
+ resp = FfiClient.instance.request(req)
43
43
  cb = await queue.wait_for(
44
44
  lambda e: e.capture_audio_frame.async_id
45
45
  == resp.capture_audio_frame.async_id
46
46
  )
47
47
  finally:
48
- ffi_client.queue.unsubscribe(queue)
48
+ FfiClient.instance.queue.unsubscribe(queue)
49
49
 
50
50
  if cb.capture_audio_frame.error:
51
51
  raise Exception(cb.capture_audio_frame.error)
@@ -15,10 +15,10 @@
15
15
  import asyncio
16
16
  from typing import Optional
17
17
 
18
- from ._ffi_client import FfiHandle, ffi_client
18
+ from ._ffi_client import FfiHandle, FfiClient
19
19
  from ._proto import audio_frame_pb2 as proto_audio_frame
20
20
  from ._proto import ffi_pb2 as proto_ffi
21
- from ._utils import RingQueue
21
+ from ._utils import RingQueue, task_done_logger
22
22
  from .audio_frame import AudioFrame
23
23
  from .track import Track
24
24
 
@@ -32,23 +32,24 @@ class AudioStream:
32
32
  ) -> None:
33
33
  self._track = track
34
34
  self._loop = loop or asyncio.get_event_loop()
35
- self._ffi_queue = ffi_client.queue.subscribe(self._loop)
35
+ self._ffi_queue = FfiClient.instance.queue.subscribe(self._loop)
36
36
  self._queue: RingQueue[AudioFrame] = RingQueue(capacity)
37
37
 
38
38
  req = proto_ffi.FfiRequest()
39
39
  new_audio_stream = req.new_audio_stream
40
40
  new_audio_stream.track_handle = track._ffi_handle.handle
41
41
  new_audio_stream.type = proto_audio_frame.AudioStreamType.AUDIO_STREAM_NATIVE
42
- resp = ffi_client.request(req)
42
+ resp = FfiClient.instance.request(req)
43
43
 
44
44
  stream_info = resp.new_audio_stream.stream
45
45
  self._ffi_handle = FfiHandle(stream_info.handle.id)
46
46
  self._info = stream_info
47
47
 
48
48
  self._task = self._loop.create_task(self._run())
49
+ self._task.add_done_callback(task_done_logger)
49
50
 
50
51
  def __del__(self) -> None:
51
- ffi_client.queue.unsubscribe(self._ffi_queue)
52
+ FfiClient.instance.queue.unsubscribe(self._ffi_queue)
52
53
 
53
54
  async def _run(self):
54
55
  while True:
@@ -62,7 +63,7 @@ class AudioStream:
62
63
  elif audio_event.HasField("eos"):
63
64
  break
64
65
 
65
- ffi_client.queue.unsubscribe(self._ffi_queue)
66
+ FfiClient.instance.queue.unsubscribe(self._ffi_queue)
66
67
 
67
68
  async def aclose(self):
68
69
  self._ffi_handle.dispose()
@@ -15,7 +15,7 @@
15
15
  from dataclasses import dataclass, field
16
16
  from typing import List, Optional
17
17
 
18
- from ._ffi_client import ffi_client
18
+ from ._ffi_client import FfiClient
19
19
  from ._proto import e2ee_pb2 as proto_e2ee
20
20
  from ._proto import ffi_pb2 as proto_ffi
21
21
 
@@ -52,13 +52,13 @@ class KeyProvider:
52
52
  req.e2ee.room_handle = self._room_handle
53
53
  req.e2ee.set_shared_key.key_index = key_index
54
54
  req.e2ee.set_shared_key.shared_key = key
55
- ffi_client.request(req)
55
+ FfiClient.instance.request(req)
56
56
 
57
57
  def export_shared_key(self, key_index: int) -> bytes:
58
58
  req = proto_ffi.FfiRequest()
59
59
  req.e2ee.room_handle = self._room_handle
60
60
  req.e2ee.get_shared_key.key_index = key_index
61
- resp = ffi_client.request(req)
61
+ resp = FfiClient.instance.request(req)
62
62
  key = resp.e2ee.get_shared_key.key
63
63
  return key
64
64
 
@@ -67,7 +67,7 @@ class KeyProvider:
67
67
  req.e2ee.room_handle = self._room_handle
68
68
  req.e2ee.ratchet_shared_key.key_index = key_index
69
69
 
70
- resp = ffi_client.request(req)
70
+ resp = FfiClient.instance.request(req)
71
71
 
72
72
  new_key = resp.e2ee.ratchet_shared_key.new_key
73
73
  return new_key
@@ -80,14 +80,14 @@ class KeyProvider:
80
80
  req.e2ee.set_key.key = key
81
81
 
82
82
  self.key_index = key_index
83
- ffi_client.request(req)
83
+ FfiClient.instance.request(req)
84
84
 
85
85
  def export_key(self, participant_identity: str, key_index: int) -> bytes:
86
86
  req = proto_ffi.FfiRequest()
87
87
  req.e2ee.room_handle = self._room_handle
88
88
  req.e2ee.get_key.participant_identity = participant_identity
89
89
  req.e2ee.get_key.key_index = key_index
90
- resp = ffi_client.request(req)
90
+ resp = FfiClient.instance.request(req)
91
91
  key = resp.e2ee.get_key.key
92
92
  return key
93
93
 
@@ -97,7 +97,7 @@ class KeyProvider:
97
97
  req.e2ee.ratchet_key.participant_identity = participant_identity
98
98
  req.e2ee.ratchet_key.key_index = key_index
99
99
 
100
- resp = ffi_client.request(req)
100
+ resp = FfiClient.instance.request(req)
101
101
  new_key = resp.e2ee.ratchet_key.new_key
102
102
  return new_key
103
103
 
@@ -129,7 +129,7 @@ class FrameCryptor:
129
129
  req.e2ee.room_handle = self._room_handle
130
130
  req.e2ee.cryptor_set_enabled.participant_identity = self._participant_identity
131
131
  req.e2ee.cryptor_set_enabled.enabled = enabled
132
- ffi_client.request(req)
132
+ FfiClient.instance.request(req)
133
133
 
134
134
  def set_key_index(self, key_index: int) -> None:
135
135
  self._key_index = key_index
@@ -137,7 +137,7 @@ class FrameCryptor:
137
137
  req.e2ee.room_handle = self._room_handle
138
138
  req.e2ee.cryptor_set_key_index.participant_identity = self._participant_identity
139
139
  req.e2ee.cryptor_set_key_index.key_index = key_index
140
- ffi_client.request(req)
140
+ FfiClient.instance.request(req)
141
141
 
142
142
 
143
143
  class E2EEManager:
@@ -164,13 +164,13 @@ class E2EEManager:
164
164
  req = proto_ffi.FfiRequest()
165
165
  req.e2ee.room_handle = self._room_handle
166
166
  req.e2ee.manager_set_enabled.enabled = enabled
167
- ffi_client.request(req)
167
+ FfiClient.instance.request(req)
168
168
 
169
169
  def frame_cryptors(self) -> List[FrameCryptor]:
170
170
  req = proto_ffi.FfiRequest()
171
171
  req.e2ee.room_handle = self._room_handle
172
172
 
173
- resp = ffi_client.request(req)
173
+ resp = FfiClient.instance.request(req)
174
174
  frame_cryptors = []
175
175
  for frame_cryptor in resp.e2ee.manager_get_frame_cryptors.frame_cryptors:
176
176
  frame_cryptors.append(
@@ -15,7 +15,7 @@
15
15
  import ctypes
16
16
  from typing import List, Union
17
17
 
18
- from ._ffi_client import FfiHandle, ffi_client
18
+ from ._ffi_client import FfiHandle, FfiClient
19
19
  from ._proto import ffi_pb2 as proto_ffi
20
20
  from ._proto import participant_pb2 as proto_participant
21
21
  from ._proto.room_pb2 import DataPacketKind, TrackPublishOptions
@@ -105,14 +105,14 @@ class LocalParticipant(Participant):
105
105
 
106
106
  req.publish_data.destination_sids.extend(sids)
107
107
 
108
- queue = ffi_client.queue.subscribe()
108
+ queue = FfiClient.instance.queue.subscribe()
109
109
  try:
110
- resp = ffi_client.request(req)
110
+ resp = FfiClient.instance.request(req)
111
111
  cb = await queue.wait_for(
112
112
  lambda e: e.publish_data.async_id == resp.publish_data.async_id
113
113
  )
114
114
  finally:
115
- ffi_client.queue.unsubscribe(queue)
115
+ FfiClient.instance.queue.unsubscribe(queue)
116
116
 
117
117
  if cb.publish_data.error:
118
118
  raise PublishDataError(cb.publish_data.error)
@@ -122,30 +122,30 @@ class LocalParticipant(Participant):
122
122
  req.update_local_metadata.local_participant_handle = self._ffi_handle.handle
123
123
  req.update_local_metadata.metadata = metadata
124
124
 
125
- queue = ffi_client.queue.subscribe()
125
+ queue = FfiClient.instance.queue.subscribe()
126
126
  try:
127
- resp = ffi_client.request(req)
127
+ resp = FfiClient.instance.request(req)
128
128
  await queue.wait_for(
129
129
  lambda e: e.update_local_metadata.async_id
130
130
  == resp.update_local_metadata.async_id
131
131
  )
132
132
  finally:
133
- ffi_client.queue.unsubscribe(queue)
133
+ FfiClient.instance.queue.unsubscribe(queue)
134
134
 
135
135
  async def update_name(self, name: str) -> None:
136
136
  req = proto_ffi.FfiRequest()
137
137
  req.update_local_name.local_participant_handle = self._ffi_handle.handle
138
138
  req.update_local_name.name = name
139
139
 
140
- queue = ffi_client.queue.subscribe()
140
+ queue = FfiClient.instance.queue.subscribe()
141
141
  try:
142
- resp = ffi_client.request(req)
142
+ resp = FfiClient.instance.request(req)
143
143
  await queue.wait_for(
144
144
  lambda e: e.update_local_name.async_id
145
145
  == resp.update_local_name.async_id
146
146
  )
147
147
  finally:
148
- ffi_client.queue.unsubscribe(queue)
148
+ FfiClient.instance.queue.unsubscribe(queue)
149
149
 
150
150
  async def publish_track(
151
151
  self, track: LocalTrack, options: TrackPublishOptions
@@ -157,7 +157,7 @@ class LocalParticipant(Participant):
157
157
 
158
158
  queue = self._room_queue.subscribe()
159
159
  try:
160
- resp = ffi_client.request(req)
160
+ resp = FfiClient.instance.request(req)
161
161
  cb = await queue.wait_for(
162
162
  lambda e: e.publish_track.async_id == resp.publish_track.async_id
163
163
  )
@@ -167,6 +167,7 @@ class LocalParticipant(Participant):
167
167
 
168
168
  track_publication = LocalTrackPublication(cb.publish_track.publication)
169
169
  track_publication.track = track
170
+ track._info.sid = track_publication.sid
170
171
  self.tracks[track_publication.sid] = track_publication
171
172
 
172
173
  queue.task_done()
@@ -181,7 +182,7 @@ class LocalParticipant(Participant):
181
182
 
182
183
  queue = self._room_queue.subscribe()
183
184
  try:
184
- resp = ffi_client.request(req)
185
+ resp = FfiClient.instance.request(req)
185
186
  cb = await queue.wait_for(
186
187
  lambda e: e.unpublish_track.async_id == resp.unpublish_track.async_id
187
188
  )
@@ -0,0 +1 @@
1
+ """ Used by importlib.resources and setuptools """
@@ -17,7 +17,7 @@ import ctypes
17
17
  import logging
18
18
  from dataclasses import dataclass, field
19
19
  from typing import Dict, Optional, Literal
20
- from ._ffi_client import FfiHandle, ffi_client
20
+ from ._ffi_client import FfiHandle, FfiClient
21
21
  from ._proto import ffi_pb2 as proto_ffi
22
22
  from ._proto import participant_pb2 as proto_participant
23
23
  from ._proto import room_pb2 as proto_room
@@ -76,6 +76,16 @@ class RoomOptions:
76
76
  rtc_config: Optional[RtcConfiguration] = None
77
77
 
78
78
 
79
+ @dataclass
80
+ class DataPacket:
81
+ data: bytes
82
+ kind: proto_room.DataPacketKind.ValueType
83
+ participant: Optional[
84
+ RemoteParticipant
85
+ ] = None # None when the data has been sent by a server SDK
86
+ topic: Optional[str] = None
87
+
88
+
79
89
  class ConnectError(Exception):
80
90
  def __init__(self, message: str):
81
91
  self.message = message
@@ -95,7 +105,7 @@ class Room(EventEmitter[EventTypes]):
95
105
 
96
106
  def __del__(self) -> None:
97
107
  if self._ffi_handle is not None:
98
- ffi_client.queue.unsubscribe(self._ffi_queue)
108
+ FfiClient.instance.queue.unsubscribe(self._ffi_queue)
99
109
 
100
110
  @property
101
111
  def sid(self) -> str:
@@ -157,19 +167,19 @@ class Room(EventEmitter[EventTypes]):
157
167
  )
158
168
 
159
169
  # subscribe before connecting so we don't miss any events
160
- self._ffi_queue = ffi_client.queue.subscribe(self._loop)
170
+ self._ffi_queue = FfiClient.instance.queue.subscribe(self._loop)
161
171
 
162
- queue = ffi_client.queue.subscribe()
172
+ queue = FfiClient.instance.queue.subscribe()
163
173
  try:
164
- resp = ffi_client.request(req)
174
+ resp = FfiClient.instance.request(req)
165
175
  cb = await queue.wait_for(
166
176
  lambda e: e.connect.async_id == resp.connect.async_id
167
177
  )
168
178
  finally:
169
- ffi_client.queue.unsubscribe(queue)
179
+ FfiClient.instance.queue.unsubscribe(queue)
170
180
 
171
181
  if cb.connect.error:
172
- ffi_client.queue.unsubscribe(self._ffi_queue)
182
+ FfiClient.instance.queue.unsubscribe(self._ffi_queue)
173
183
  raise ConnectError(cb.connect.error)
174
184
 
175
185
  self._ffi_handle = FfiHandle(cb.connect.room.handle.id)
@@ -201,17 +211,17 @@ class Room(EventEmitter[EventTypes]):
201
211
  req = proto_ffi.FfiRequest()
202
212
  req.disconnect.room_handle = self._ffi_handle.handle # type: ignore
203
213
 
204
- queue = ffi_client.queue.subscribe()
214
+ queue = FfiClient.instance.queue.subscribe()
205
215
  try:
206
- resp = ffi_client.request(req)
216
+ resp = FfiClient.instance.request(req)
207
217
  await queue.wait_for(
208
218
  lambda e: e.disconnect.async_id == resp.disconnect.async_id
209
219
  )
210
220
  finally:
211
- ffi_client.queue.unsubscribe(queue)
221
+ FfiClient.instance.queue.unsubscribe(queue)
212
222
 
213
223
  await self._task
214
- ffi_client.queue.unsubscribe(self._ffi_queue)
224
+ FfiClient.instance.queue.unsubscribe(self._ffi_queue)
215
225
 
216
226
  async def _listen_task(self) -> None:
217
227
  # listen to incoming room events
@@ -372,10 +382,12 @@ class Room(EventEmitter[EventTypes]):
372
382
  rparticipant = self.participants[event.data_received.participant_sid]
373
383
  self.emit(
374
384
  "data_received",
375
- data,
376
- event.data_received.kind,
377
- rparticipant,
378
- event.data_received.topic,
385
+ DataPacket(
386
+ data=data,
387
+ kind=event.data_received.kind,
388
+ participant=rparticipant,
389
+ topic=event.data_received.topic,
390
+ ),
379
391
  )
380
392
  elif which == "e2ee_state_changed":
381
393
  sid = event.e2ee_state_changed.participant_sid
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from typing import TYPE_CHECKING, List, Union
16
- from ._ffi_client import FfiHandle, ffi_client
16
+ from ._ffi_client import FfiHandle, FfiClient
17
17
  from ._proto import ffi_pb2 as proto_ffi
18
18
  from ._proto import track_pb2 as proto_track
19
19
  from ._proto import stats_pb2 as proto_stats
@@ -52,14 +52,14 @@ class Track:
52
52
  req = proto_ffi.FfiRequest()
53
53
  req.get_stats.track_handle = self._ffi_handle.handle
54
54
 
55
- queue = ffi_client.queue.subscribe()
55
+ queue = FfiClient.instance.queue.subscribe()
56
56
  try:
57
- resp = ffi_client.request(req)
57
+ resp = FfiClient.instance.request(req)
58
58
  cb = await queue.wait_for(
59
59
  lambda e: e.get_stats.async_id == resp.get_stats.async_id
60
60
  )
61
61
  finally:
62
- ffi_client.queue.unsubscribe(queue)
62
+ FfiClient.instance.queue.unsubscribe(queue)
63
63
 
64
64
  if cb.get_stats.error:
65
65
  raise Exception(cb.get_stats.error)
@@ -77,7 +77,7 @@ class LocalAudioTrack(Track):
77
77
  req.create_audio_track.name = name
78
78
  req.create_audio_track.source_handle = source._ffi_handle.handle
79
79
 
80
- resp = ffi_client.request(req)
80
+ resp = FfiClient.instance.request(req)
81
81
  return LocalAudioTrack(resp.create_audio_track.track)
82
82
 
83
83
 
@@ -91,7 +91,7 @@ class LocalVideoTrack(Track):
91
91
  req.create_video_track.name = name
92
92
  req.create_video_track.source_handle = source._ffi_handle.handle
93
93
 
94
- resp = ffi_client.request(req)
94
+ resp = FfiClient.instance.request(req)
95
95
  return LocalVideoTrack(resp.create_video_track.track)
96
96
 
97
97
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  from typing import Optional
16
16
 
17
- from ._ffi_client import FfiHandle, ffi_client
17
+ from ._ffi_client import FfiHandle, FfiClient
18
18
  from ._proto import e2ee_pb2 as proto_e2ee
19
19
  from ._proto import ffi_pb2 as proto_ffi
20
20
  from ._proto import track_pb2 as proto_track
@@ -82,4 +82,4 @@ class RemoteTrackPublication(TrackPublication):
82
82
  req = proto_ffi.FfiRequest()
83
83
  req.set_subscribed.subscribe = subscribed
84
84
  req.set_subscribed.publication_handle = self._ffi_handle.handle
85
- ffi_client.request(req)
85
+ FfiClient.instance.request(req)
@@ -0,0 +1 @@
1
+ __version__ = "0.7.1.dev1"
@@ -15,7 +15,7 @@
15
15
  import ctypes
16
16
  from typing import Union
17
17
 
18
- from ._ffi_client import FfiHandle, ffi_client
18
+ from ._ffi_client import FfiHandle, FfiClient
19
19
  from ._proto import ffi_pb2 as proto_ffi
20
20
  from ._utils import get_address
21
21
  from ._proto import video_frame_pb2 as proto_video_frame
@@ -75,7 +75,7 @@ class VideoFrameBuffer(ABC):
75
75
  def to_i420(self) -> "I420Buffer":
76
76
  req = proto_ffi.FfiRequest()
77
77
  req.to_i420.buffer.CopyFrom(self._proto_info())
78
- resp = ffi_client.request(req)
78
+ resp = FfiClient.instance.request(req)
79
79
  return I420Buffer._from_owned_info(resp.to_i420.buffer)
80
80
 
81
81
  def to_argb(self, dst: "ArgbFrame") -> None:
@@ -86,7 +86,7 @@ class VideoFrameBuffer(ABC):
86
86
  req.to_argb.dst_stride = dst.stride
87
87
  req.to_argb.dst_width = dst.width
88
88
  req.to_argb.dst_height = dst.height
89
- ffi_client.request(req)
89
+ FfiClient.instance.request(req)
90
90
 
91
91
  @staticmethod
92
92
  def _from_owned_info(
@@ -139,7 +139,7 @@ class NativeVideoBuffer(VideoFrameBuffer):
139
139
  def to_i420(self) -> "I420Buffer":
140
140
  req = proto_ffi.FfiRequest()
141
141
  req.to_i420.handle = self._ffi_handle.handle
142
- resp = ffi_client.request(req)
142
+ resp = FfiClient.instance.request(req)
143
143
  return I420Buffer._from_owned_info(resp.to_i420.buffer)
144
144
 
145
145
  def to_argb(self, dst: "ArgbFrame") -> None:
@@ -768,7 +768,7 @@ class ArgbFrame:
768
768
  req.to_i420.argb.height = self.height
769
769
  req.to_i420.argb.stride = self.stride
770
770
  req.to_i420.argb.ptr = get_address(memoryview(self._data))
771
- res = ffi_client.request(req)
771
+ res = FfiClient.instance.request(req)
772
772
  return I420Buffer._from_owned_info(res.to_i420.buffer)
773
773
 
774
774
  @property
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from ._ffi_client import FfiHandle, ffi_client
15
+ from ._ffi_client import FfiHandle, FfiClient
16
16
  from ._proto import ffi_pb2 as proto_ffi
17
17
  from ._proto import video_frame_pb2 as proto_video_frame
18
18
  from .video_frame import VideoFrame
@@ -27,7 +27,7 @@ class VideoSource:
27
27
  req.new_video_source.resolution.width = width
28
28
  req.new_video_source.resolution.height = height
29
29
 
30
- resp = ffi_client.request(req)
30
+ resp = FfiClient.instance.request(req)
31
31
  self._info = resp.new_video_source.source
32
32
  self._ffi_handle = FfiHandle(self._info.handle.id)
33
33
 
@@ -37,4 +37,4 @@ class VideoSource:
37
37
  req.capture_video_frame.info.CopyFrom(frame.buffer._proto_info())
38
38
  req.capture_video_frame.frame.rotation = frame.rotation
39
39
  req.capture_video_frame.frame.timestamp_us = frame.timestamp_us
40
- ffi_client.request(req)
40
+ FfiClient.instance.request(req)
@@ -15,10 +15,10 @@
15
15
  import asyncio
16
16
  from typing import Optional
17
17
 
18
- from ._ffi_client import FfiHandle, ffi_client
18
+ from ._ffi_client import FfiHandle, FfiClient
19
19
  from ._proto import ffi_pb2 as proto_ffi
20
20
  from ._proto import video_frame_pb2 as proto_video_frame
21
- from ._utils import RingQueue
21
+ from ._utils import RingQueue, task_done_logger
22
22
  from .track import Track
23
23
  from .video_frame import VideoFrame, VideoFrameBuffer
24
24
 
@@ -32,22 +32,23 @@ class VideoStream:
32
32
  ) -> None:
33
33
  self._track = track
34
34
  self._loop = loop or asyncio.get_event_loop()
35
- self._ffi_queue = ffi_client.queue.subscribe(self._loop)
35
+ self._ffi_queue = FfiClient.instance.queue.subscribe(self._loop)
36
36
  self._queue: RingQueue[VideoFrame] = RingQueue(capacity)
37
37
 
38
38
  req = proto_ffi.FfiRequest()
39
39
  new_video_stream = req.new_video_stream
40
40
  new_video_stream.track_handle = track._ffi_handle.handle
41
41
  new_video_stream.type = proto_video_frame.VideoStreamType.VIDEO_STREAM_NATIVE
42
- resp = ffi_client.request(req)
42
+ resp = FfiClient.instance.request(req)
43
43
 
44
44
  stream_info = resp.new_video_stream.stream
45
45
  self._ffi_handle = FfiHandle(stream_info.handle.id)
46
46
  self._info = stream_info.info
47
47
  self._task = self._loop.create_task(self._run())
48
+ self._task.add_done_callback(task_done_logger)
48
49
 
49
50
  def __del__(self) -> None:
50
- ffi_client.queue.unsubscribe(self._ffi_queue)
51
+ FfiClient.instance.queue.unsubscribe(self._ffi_queue)
51
52
 
52
53
  async def _run(self):
53
54
  while True:
@@ -67,7 +68,7 @@ class VideoStream:
67
68
  elif video_event.HasField("eos"):
68
69
  break
69
70
 
70
- ffi_client.queue.unsubscribe(self._ffi_queue)
71
+ FfiClient.instance.queue.unsubscribe(self._ffi_queue)
71
72
 
72
73
  async def aclose(self):
73
74
  self._ffi_handle.dispose()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: livekit
3
- Version: 0.7.0.dev1
3
+ Version: 0.7.1.dev1
4
4
  Summary: Python Real-time SDK for LiveKit
5
5
  Home-page: https://github.com/livekit/python-sdks
6
6
  License: Apache-2.0
@@ -19,8 +19,8 @@ Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3 :: Only
20
20
  Requires-Python: >=3.9.0
21
21
  Description-Content-Type: text/markdown
22
- Requires-Dist: protobuf>=3.1.0
23
- Requires-Dist: types-protobuf>=3.1.0
22
+ Requires-Dist: protobuf>=4
23
+ Requires-Dist: types-protobuf>=4
24
24
 
25
25
  # LiveKit Real-time Python SDK
26
26
 
@@ -40,4 +40,5 @@ livekit/rtc/_proto/stats_pb2.pyi
40
40
  livekit/rtc/_proto/track_pb2.py
41
41
  livekit/rtc/_proto/track_pb2.pyi
42
42
  livekit/rtc/_proto/video_frame_pb2.py
43
- livekit/rtc/_proto/video_frame_pb2.pyi
43
+ livekit/rtc/_proto/video_frame_pb2.pyi
44
+ livekit/rtc/resources/__init__.py
@@ -0,0 +1,2 @@
1
+ protobuf>=4
2
+ types-protobuf>=4
@@ -10,6 +10,8 @@ build-backend = "setuptools.build_meta"
10
10
  build = "cp39-*"
11
11
  skip = "*-musllinux_*" # not supported (libwebrtc is using glibc)
12
12
 
13
+ before-build = "pip install requests && python rust-sdks/download_ffi.py --output livekit/rtc/resources"
14
+
13
15
  manylinux-x86_64-image = "manylinux_2_28"
14
16
  manylinux-i686-image = "manylinux_2_28"
15
17
  manylinux-aarch64-image = "manylinux_2_28"
@@ -14,8 +14,6 @@
14
14
 
15
15
  import os
16
16
  import pathlib
17
- import subprocess
18
-
19
17
  import setuptools
20
18
  import setuptools.command.build_py
21
19
  from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
@@ -33,30 +31,6 @@ class bdist_wheel(_bdist_wheel):
33
31
  _bdist_wheel.finalize_options(self)
34
32
 
35
33
 
36
- class BuildPyCommand(setuptools.command.build_py.build_py):
37
- """Download a prebuilt version of livekit_ffi"""
38
-
39
- def run(self):
40
- download_script = here / "rust-sdks" / "download_ffi.py"
41
- output = here / "livekit" / "rtc" / "resources"
42
- cmd = [
43
- "python3",
44
- str(download_script.absolute()),
45
- "--output",
46
- str(output.absolute()),
47
- ]
48
-
49
- # cibuildwheel is crosscompiling to arm64 on macos, make sure we download the
50
- # right binary (kind of a hack here...)
51
- if os.environ.get("CIBUILDWHEEL") == "1" and "arm64" in os.environ.get(
52
- "ARCHFLAGS", ""
53
- ):
54
- cmd += ["--arch", "arm64"]
55
-
56
- subprocess.run(cmd, check=True)
57
- setuptools.command.build_py.build_py.run(self)
58
-
59
-
60
34
  setuptools.setup(
61
35
  name="livekit",
62
36
  version=about["__version__"],
@@ -66,7 +40,6 @@ setuptools.setup(
66
40
  url="https://github.com/livekit/python-sdks",
67
41
  cmdclass={
68
42
  "bdist_wheel": bdist_wheel,
69
- "build_py": BuildPyCommand,
70
43
  },
71
44
  classifiers=[
72
45
  "Intended Audience :: Developers",
@@ -83,9 +56,10 @@ setuptools.setup(
83
56
  license="Apache-2.0",
84
57
  packages=setuptools.find_namespace_packages(include=["livekit.*"]),
85
58
  python_requires=">=3.9.0",
86
- install_requires=["protobuf>=3.1.0", "types-protobuf>=3.1.0"],
59
+ install_requires=["protobuf>=4", "types-protobuf>=4"],
87
60
  package_data={
88
- "livekit.rtc": ["resources/*", "_proto/*.py", "py.typed", "*.pyi", "**/*.pyi"],
61
+ "livekit.rtc": ["_proto/*.py", "py.typed", "*.pyi", "**/*.pyi"],
62
+ "livekit.rtc.resources": ["*.so", "*.dylib", "*.dll", "LICENSE.md", "*.h"],
89
63
  },
90
64
  project_urls={
91
65
  "Documentation": "https://docs.livekit.io",
@@ -1 +0,0 @@
1
- __version__ = "0.7.0.dev1"
@@ -1,2 +0,0 @@
1
- protobuf>=3.1.0
2
- types-protobuf>=3.1.0
File without changes
File without changes