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
|
@@ -0,0 +1,354 @@
|
|
|
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 asyncio
|
|
18
|
+
import json
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any, AsyncIterator, Optional
|
|
21
|
+
|
|
22
|
+
from ._ffi_client import FfiClient, FfiHandle
|
|
23
|
+
from ._proto import audio_frame_pb2 as proto_audio_frame
|
|
24
|
+
from ._proto import ffi_pb2 as proto_ffi
|
|
25
|
+
from ._proto.track_pb2 import TrackSource
|
|
26
|
+
from ._utils import RingQueue, task_done_logger
|
|
27
|
+
from .audio_frame import AudioFrame
|
|
28
|
+
from .log import logger
|
|
29
|
+
from .participant import Participant
|
|
30
|
+
from .track import Track
|
|
31
|
+
from .frame_processor import FrameProcessor
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class AudioFrameEvent:
|
|
36
|
+
"""An event representing a received audio frame.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
frame (AudioFrame): The received audio frame.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
frame: AudioFrame
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class NoiseCancellationOptions:
|
|
47
|
+
module_id: str
|
|
48
|
+
options: dict[str, Any]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AudioStream:
|
|
52
|
+
"""An asynchronous audio stream for receiving audio frames from a participant or track.
|
|
53
|
+
|
|
54
|
+
The `AudioStream` class provides an asynchronous iterator over audio frames received from
|
|
55
|
+
a specific track or participant. It allows you to receive audio frames in real-time with
|
|
56
|
+
customizable sample rates and channel configurations.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
track: Track,
|
|
62
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
63
|
+
capacity: int = 0,
|
|
64
|
+
sample_rate: int = 48000,
|
|
65
|
+
num_channels: int = 1,
|
|
66
|
+
frame_size_ms: int | None = None,
|
|
67
|
+
noise_cancellation: Optional[NoiseCancellationOptions | FrameProcessor[AudioFrame]] = None,
|
|
68
|
+
auto_close_noise_cancellation: bool = True,
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize an `AudioStream` instance.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
track (Optional[Track]): The audio track from which to receive audio. If not provided,
|
|
75
|
+
you must specify `participant` and `track_source` in `kwargs`.
|
|
76
|
+
loop (Optional[asyncio.AbstractEventLoop], optional): The event loop to use.
|
|
77
|
+
Defaults to the current event loop.
|
|
78
|
+
capacity (int, optional): The capacity of the internal frame queue. Defaults to 0 (unbounded).
|
|
79
|
+
sample_rate (int, optional): The sample rate for the audio stream in Hz.
|
|
80
|
+
Defaults to 48000.
|
|
81
|
+
num_channels (int, optional): The number of audio channels. Defaults to 1.
|
|
82
|
+
noise_cancellation (Optional[NoiseCancellationOptions | FrameProcessor[AudioFrame]], optional):
|
|
83
|
+
If noise cancellation is used, pass a `NoiseCancellationOptions` or `FrameProcessor[AudioFrame]` instance
|
|
84
|
+
created by the noise cancellation module.
|
|
85
|
+
auto_close_noise_cancellation (bool):
|
|
86
|
+
When the audio stream closes, should the FrameProcessor's close method be run? If
|
|
87
|
+
False, then the frame processor can be reused with another AudioStream.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
```python
|
|
91
|
+
audio_stream = AudioStream(
|
|
92
|
+
track=audio_track,
|
|
93
|
+
sample_rate=44100,
|
|
94
|
+
num_channels=2,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
audio_stream = AudioStream.from_track(
|
|
98
|
+
track=audio_track,
|
|
99
|
+
sample_rate=44100,
|
|
100
|
+
num_channels=2,
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
"""
|
|
104
|
+
self._track: Track | None = track
|
|
105
|
+
self._sample_rate = sample_rate
|
|
106
|
+
self._num_channels = num_channels
|
|
107
|
+
self._frame_size_ms = frame_size_ms
|
|
108
|
+
self._loop = loop or asyncio.get_event_loop()
|
|
109
|
+
# Subscribe before creating the stream so none of its events are missed. Its
|
|
110
|
+
# handle is only known once the request returns; until then every audio stream
|
|
111
|
+
# event is let through, and _run drops the ones of other streams.
|
|
112
|
+
handle_id: int | None = None
|
|
113
|
+
self._ffi_queue = FfiClient.instance.queue.subscribe(
|
|
114
|
+
self._loop,
|
|
115
|
+
filter_fn=lambda e: (
|
|
116
|
+
e.WhichOneof("message") == "audio_stream_event"
|
|
117
|
+
and (handle_id is None or e.audio_stream_event.stream_handle == handle_id)
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
self._queue: RingQueue[AudioFrameEvent | None] = RingQueue(capacity)
|
|
121
|
+
|
|
122
|
+
self._audio_filter_module: str | None = None
|
|
123
|
+
self._audio_filter_options: dict[str, Any] | None = None
|
|
124
|
+
self._processor: FrameProcessor[AudioFrame] | None = None
|
|
125
|
+
self._processor_auto_close = True
|
|
126
|
+
if isinstance(noise_cancellation, NoiseCancellationOptions):
|
|
127
|
+
self._audio_filter_module = noise_cancellation.module_id
|
|
128
|
+
self._audio_filter_options = noise_cancellation.options
|
|
129
|
+
elif isinstance(noise_cancellation, FrameProcessor):
|
|
130
|
+
self._processor = noise_cancellation
|
|
131
|
+
self._processor_auto_close = auto_close_noise_cancellation
|
|
132
|
+
|
|
133
|
+
stream: Any = None
|
|
134
|
+
if "participant" in kwargs:
|
|
135
|
+
stream = self._create_owned_stream_from_participant(
|
|
136
|
+
participant=kwargs["participant"], track_source=kwargs["track_source"]
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
stream = self._create_owned_stream()
|
|
140
|
+
self._ffi_handle = FfiHandle(stream.handle.id)
|
|
141
|
+
handle_id = stream.handle.id
|
|
142
|
+
self._info = stream.info
|
|
143
|
+
|
|
144
|
+
# Start _run only after _ffi_handle is set, so a failure above doesn't orphan
|
|
145
|
+
# a task that dereferences an unassigned _ffi_handle.
|
|
146
|
+
self._task = self._loop.create_task(self._run())
|
|
147
|
+
self._task.add_done_callback(task_done_logger)
|
|
148
|
+
|
|
149
|
+
if self._track is not None:
|
|
150
|
+
self._track._register_audio_stream(self)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_participant(
|
|
154
|
+
cls,
|
|
155
|
+
*,
|
|
156
|
+
participant: Participant,
|
|
157
|
+
track_source: TrackSource.ValueType,
|
|
158
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
159
|
+
capacity: int = 0,
|
|
160
|
+
sample_rate: int = 48000,
|
|
161
|
+
num_channels: int = 1,
|
|
162
|
+
frame_size_ms: int | None = None,
|
|
163
|
+
noise_cancellation: Optional[NoiseCancellationOptions | FrameProcessor[AudioFrame]] = None,
|
|
164
|
+
auto_close_noise_cancellation: bool = True,
|
|
165
|
+
) -> AudioStream:
|
|
166
|
+
"""Create an `AudioStream` from a participant's audio track.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
participant (Participant): The participant from whom to receive audio.
|
|
170
|
+
track_source (TrackSource.ValueType): The source of the audio track (e.g., microphone, screen share).
|
|
171
|
+
loop (Optional[asyncio.AbstractEventLoop], optional): The event loop to use. Defaults to the current event loop.
|
|
172
|
+
capacity (int, optional): The capacity of the internal frame queue. Defaults to 0 (unbounded).
|
|
173
|
+
sample_rate (int, optional): The sample rate for the audio stream in Hz. Defaults to 48000.
|
|
174
|
+
num_channels (int, optional): The number of audio channels. Defaults to 1.
|
|
175
|
+
noise_cancellation (Optional[NoiseCancellationOptions], optional):
|
|
176
|
+
If noise cancellation is used, pass a `NoiseCancellationOptions` instance
|
|
177
|
+
created by the noise cancellation module.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
AudioStream: An instance of `AudioStream` that can be used to receive audio frames.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
```python
|
|
184
|
+
audio_stream = AudioStream.from_participant(
|
|
185
|
+
participant=participant,
|
|
186
|
+
track_source=TrackSource.MICROPHONE,
|
|
187
|
+
sample_rate=24000,
|
|
188
|
+
num_channels=1,
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
"""
|
|
192
|
+
return AudioStream(
|
|
193
|
+
participant=participant,
|
|
194
|
+
track_source=track_source,
|
|
195
|
+
loop=loop,
|
|
196
|
+
capacity=capacity,
|
|
197
|
+
track=None, # type: ignore
|
|
198
|
+
sample_rate=sample_rate,
|
|
199
|
+
num_channels=num_channels,
|
|
200
|
+
frame_size_ms=frame_size_ms,
|
|
201
|
+
noise_cancellation=noise_cancellation,
|
|
202
|
+
auto_close_noise_cancellation=auto_close_noise_cancellation,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_track(
|
|
207
|
+
cls,
|
|
208
|
+
*,
|
|
209
|
+
track: Track,
|
|
210
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
211
|
+
capacity: int = 0,
|
|
212
|
+
sample_rate: int = 48000,
|
|
213
|
+
num_channels: int = 1,
|
|
214
|
+
frame_size_ms: int | None = None,
|
|
215
|
+
noise_cancellation: Optional[NoiseCancellationOptions | FrameProcessor[AudioFrame]] = None,
|
|
216
|
+
auto_close_noise_cancellation: bool = False,
|
|
217
|
+
) -> AudioStream:
|
|
218
|
+
"""Create an `AudioStream` from an existing audio track.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
track (Track): The audio track from which to receive audio.
|
|
222
|
+
loop (Optional[asyncio.AbstractEventLoop], optional): The event loop to use. Defaults to the current event loop.
|
|
223
|
+
capacity (int, optional): The capacity of the internal frame queue. Defaults to 0 (unbounded).
|
|
224
|
+
sample_rate (int, optional): The sample rate for the audio stream in Hz. Defaults to 48000.
|
|
225
|
+
num_channels (int, optional): The number of audio channels. Defaults to 1.
|
|
226
|
+
noise_cancellation (Optional[NoiseCancellationOptions | FrameProcessor[AudioFrame]], optional):
|
|
227
|
+
If noise cancellation is used, pass a `NoiseCancellationOptions` or `FrameProcessor[AudioFrame]` instance
|
|
228
|
+
created by the noise cancellation module.
|
|
229
|
+
auto_close_noise_cancellation (bool):
|
|
230
|
+
When the audio stream closes, leaves the FrameProcessor in an unclosed state so it
|
|
231
|
+
can be used with another AudioStream.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
AudioStream: An instance of `AudioStream` that can be used to receive audio frames.
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
```python
|
|
238
|
+
audio_stream = AudioStream.from_track(
|
|
239
|
+
track=audio_track,
|
|
240
|
+
sample_rate=44100,
|
|
241
|
+
num_channels=2,
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
"""
|
|
245
|
+
return AudioStream(
|
|
246
|
+
track=track,
|
|
247
|
+
loop=loop,
|
|
248
|
+
capacity=capacity,
|
|
249
|
+
sample_rate=sample_rate,
|
|
250
|
+
num_channels=num_channels,
|
|
251
|
+
frame_size_ms=frame_size_ms,
|
|
252
|
+
noise_cancellation=noise_cancellation,
|
|
253
|
+
auto_close_noise_cancellation=auto_close_noise_cancellation,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def __del__(self) -> None:
|
|
257
|
+
# _ffi_queue is unset if __init__ raised early or the instance was built via __new__
|
|
258
|
+
ffi_queue = getattr(self, "_ffi_queue", None)
|
|
259
|
+
if ffi_queue is not None:
|
|
260
|
+
FfiClient.instance.queue.unsubscribe(ffi_queue)
|
|
261
|
+
|
|
262
|
+
def _create_owned_stream(self) -> Any:
|
|
263
|
+
assert self._track is not None
|
|
264
|
+
req = proto_ffi.FfiRequest()
|
|
265
|
+
new_audio_stream = req.new_audio_stream
|
|
266
|
+
new_audio_stream.track_handle = self._track._ffi_handle.handle
|
|
267
|
+
new_audio_stream.sample_rate = self._sample_rate
|
|
268
|
+
new_audio_stream.num_channels = self._num_channels
|
|
269
|
+
new_audio_stream.queue_size_frames = 0
|
|
270
|
+
if self._frame_size_ms:
|
|
271
|
+
new_audio_stream.frame_size_ms = self._frame_size_ms
|
|
272
|
+
new_audio_stream.type = proto_audio_frame.AudioStreamType.AUDIO_STREAM_NATIVE
|
|
273
|
+
if self._audio_filter_module is not None:
|
|
274
|
+
new_audio_stream.audio_filter_module_id = self._audio_filter_module
|
|
275
|
+
if self._audio_filter_options is not None:
|
|
276
|
+
new_audio_stream.audio_filter_options = json.dumps(self._audio_filter_options)
|
|
277
|
+
resp = FfiClient.instance.request(req)
|
|
278
|
+
return resp.new_audio_stream.stream
|
|
279
|
+
|
|
280
|
+
def _create_owned_stream_from_participant(
|
|
281
|
+
self, participant: Participant, track_source: TrackSource.ValueType
|
|
282
|
+
) -> Any:
|
|
283
|
+
req = proto_ffi.FfiRequest()
|
|
284
|
+
audio_stream_from_participant = req.audio_stream_from_participant
|
|
285
|
+
audio_stream_from_participant.participant_handle = participant._ffi_handle.handle
|
|
286
|
+
audio_stream_from_participant.sample_rate = self._sample_rate
|
|
287
|
+
audio_stream_from_participant.num_channels = self._num_channels
|
|
288
|
+
audio_stream_from_participant.queue_size_frames = 0
|
|
289
|
+
audio_stream_from_participant.type = proto_audio_frame.AudioStreamType.AUDIO_STREAM_NATIVE
|
|
290
|
+
audio_stream_from_participant.track_source = track_source
|
|
291
|
+
if self._frame_size_ms:
|
|
292
|
+
audio_stream_from_participant.frame_size_ms = self._frame_size_ms
|
|
293
|
+
|
|
294
|
+
if self._audio_filter_module is not None:
|
|
295
|
+
audio_stream_from_participant.audio_filter_module_id = self._audio_filter_module
|
|
296
|
+
if self._audio_filter_options is not None:
|
|
297
|
+
audio_stream_from_participant.audio_filter_options = json.dumps(
|
|
298
|
+
self._audio_filter_options
|
|
299
|
+
)
|
|
300
|
+
resp = FfiClient.instance.request(req)
|
|
301
|
+
return resp.audio_stream_from_participant.stream
|
|
302
|
+
|
|
303
|
+
async def _run(self) -> None:
|
|
304
|
+
while True:
|
|
305
|
+
event = await self._ffi_queue.wait_for(self._is_event)
|
|
306
|
+
audio_event: proto_audio_frame.AudioStreamEvent = event.audio_stream_event
|
|
307
|
+
|
|
308
|
+
if audio_event.HasField("frame_received"):
|
|
309
|
+
owned_buffer_info = audio_event.frame_received.frame
|
|
310
|
+
frame = AudioFrame._from_owned_info(owned_buffer_info)
|
|
311
|
+
if self._processor is not None and self._processor.enabled:
|
|
312
|
+
try:
|
|
313
|
+
frame = self._processor._process(frame)
|
|
314
|
+
except Exception:
|
|
315
|
+
logger.warning(
|
|
316
|
+
"Frame processing failed, passing through original frame",
|
|
317
|
+
exc_info=True,
|
|
318
|
+
)
|
|
319
|
+
event = AudioFrameEvent(frame)
|
|
320
|
+
self._queue.put(event)
|
|
321
|
+
elif audio_event.HasField("eos"):
|
|
322
|
+
self._queue.put(None)
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
FfiClient.instance.queue.unsubscribe(self._ffi_queue)
|
|
326
|
+
|
|
327
|
+
async def aclose(self) -> None:
|
|
328
|
+
"""Asynchronously close the audio stream.
|
|
329
|
+
|
|
330
|
+
This method cleans up resources associated with the audio stream and waits for
|
|
331
|
+
any pending operations to complete.
|
|
332
|
+
"""
|
|
333
|
+
if self._track is not None:
|
|
334
|
+
self._track._unregister_audio_stream(self)
|
|
335
|
+
self._ffi_handle.dispose()
|
|
336
|
+
await self._task
|
|
337
|
+
if self._processor is not None and self._processor_auto_close:
|
|
338
|
+
self._processor._close()
|
|
339
|
+
|
|
340
|
+
def _is_event(self, e: proto_ffi.FfiEvent) -> bool:
|
|
341
|
+
return e.audio_stream_event.stream_handle == self._ffi_handle.handle
|
|
342
|
+
|
|
343
|
+
def __aiter__(self) -> AsyncIterator[AudioFrameEvent]:
|
|
344
|
+
return self
|
|
345
|
+
|
|
346
|
+
async def __anext__(self) -> AudioFrameEvent:
|
|
347
|
+
if self._task.done():
|
|
348
|
+
raise StopAsyncIteration
|
|
349
|
+
|
|
350
|
+
item = await self._queue.get()
|
|
351
|
+
if item is None:
|
|
352
|
+
raise StopAsyncIteration
|
|
353
|
+
|
|
354
|
+
return item
|