diff --git a/.github/actions/python-uv-setup/action.yml b/.github/actions/python-uv-setup/action.yml
index 0f5f0f16..bc11270c 100644
--- a/.github/actions/python-uv-setup/action.yml
+++ b/.github/actions/python-uv-setup/action.yml
@@ -13,9 +13,34 @@ inputs:
description: Directory to run in
required: false
default: "."
+ build-extension:
+ description: Build getstream-rtc (the Rust extension) into the environment; "false" installs everything else
+ required: false
+ default: "true"
runs:
using: composite
steps:
+ - name: Install the native build dependencies
+ if: ${{ inputs.build-extension == 'true' }}
+ shell: bash
+ # libvpx is linked from the system; bindgen needs libclang to generate its bindings.
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends libvpx-dev libclang-dev
+
+ - name: Install the Rust toolchain
+ if: ${{ inputs.build-extension == 'true' }}
+ shell: bash
+ working-directory: ${{ inputs.working-directory }}/getstream-rtc
+ # Installs the toolchain pinned in getstream-rtc/rust-toolchain.toml.
+ run: rustup toolchain install
+
+ - name: Cache the Rust build
+ if: ${{ inputs.build-extension == 'true' }}
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: ${{ inputs.working-directory }}/getstream-rtc
+
- name: Install the latest version of uv
id: setup_uv
uses: astral-sh/setup-uv@v7
@@ -38,5 +63,4 @@ runs:
- name: Install the project dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
- run: uv sync ${{ inputs.sync-flags }}
-
+ run: uv sync ${{ inputs.sync-flags }} ${{ inputs.build-extension != 'true' && '--no-install-package getstream-rtc' || '' }}
diff --git a/.github/workflows/run_integration.yml b/.github/workflows/run_integration.yml
index 8526e419..11fafeec 100644
--- a/.github/workflows/run_integration.yml
+++ b/.github/workflows/run_integration.yml
@@ -31,10 +31,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
+ with:
+ persist-credentials: false
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
with:
python-version: ${{ matrix.python-version }}
+ build-extension: "false"
- name: Run integration tests
env:
STREAM_API_KEY: ${{ vars.STREAM_CHAT_API_KEY }}
@@ -55,6 +58,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
+ with:
+ persist-credentials: false
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
with:
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index a10cb469..ff6427af 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -23,8 +23,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
+ with:
+ persist-credentials: false
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
+ with:
+ build-extension: "false"
- name: Run linter
run: make lint
@@ -34,11 +38,49 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
+ with:
+ persist-credentials: false
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
+ # ty reads getstream_rtc from getstream-rtc/ (see make typecheck), not the built extension.
+ with:
+ build-extension: "false"
- name: Run type checker
run: make typecheck
+ rust:
+ name: 🦀 Rust
+ runs-on: ubuntu-latest
+ env:
+ # The same interpreter for clippy and the tests, so the PyO3 build is reused.
+ PYO3_PYTHON: /usr/bin/python3
+ defaults:
+ run:
+ # rustup reads the toolchain from the current directory.
+ working-directory: getstream-rtc
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ with:
+ persist-credentials: false
+ - name: Install the native build dependencies
+ # The tests link libpython of the system Python.
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends libvpx-dev libclang-dev python3-dev
+ - name: Install the Rust toolchain
+ run: rustup toolchain install
+ - name: Cache the Rust build
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: getstream-rtc
+ - name: Check formatting
+ run: cargo fmt --check
+ - name: Run clippy
+ run: cargo clippy --all-targets -- -D warnings
+ - name: Run the unit tests
+ run: cargo test --lib
+
# ── Non-video tests (chat, feeds, etc.) ────────────────────────────
# Uses STREAM_CHAT_* credentials which do NOT have video enabled.
# Video paths and manual test paths are defined in the Makefile.
@@ -53,10 +95,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
+ with:
+ persist-credentials: false
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
with:
python-version: ${{ matrix.python-version }}
+ build-extension: "false"
- name: Run tests
run: make test
@@ -73,6 +118,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
+ with:
+ persist-credentials: false
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
with:
diff --git a/.gitignore b/.gitignore
index dc30fdd7..c17d6321 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,5 +22,10 @@ build/
__pycache__/
*.py[cod]
*$py.class
-\n+# Local protocol folder (handled by generate_webrtc.sh)
+
+# Rust build artifacts
+target/
+getstream-rtc/getstream_rtc/*.so
+getstream-rtc/getstream_rtc/*.pyd
+# Local protocol folder (handled by generate_webrtc.sh)
protocol/
diff --git a/AGENTS.md b/AGENTS.md
index 2c11065e..2fea8e47 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,9 +1,10 @@
## Project setup
1. This project uses `uv`, `pyproject.toml` and venv to manage dependencies
-2. Never use pip directly, use `uv add` to add dependencies and `uv sync --dev --all-packages` to install the dependency
+2. Never use pip directly, use `uv add` to add dependencies and `uv sync --dev --all-packages --all-extras` to install the dependency (without `--all-extras` the sync removes the packages of the optional dependency groups, for example `aiortc`, `av` and `numpy`)
3. Do not change code generated python code, `./generate.sh` is the script responsible of rebuilding all API endpoints and API models
-4. **WebRTC Dependencies**: All dependencies related to WebRTC, audio, video processing (like `aiortc`, `numpy`, `torch`, `torchaudio`, `soundfile`, `scipy`, `deepgram-sdk`, `elevenlabs`, etc.) are organized under the `webrtc` optional dependencies group. Plugins that work with audio, video, or WebRTC functionality should depend on `getstream[webrtc]` instead of just `getstream`.
+4. **WebRTC Dependencies**: The dependencies of `getstream.video.rtc` (`getstream-rtc`, `aiortc`, `av`, `numpy`, and `twirp`, `protobuf` and `aiohttp` for the generated SFU code) are organized under the `webrtc` optional dependencies group. Plugins that work with audio, video, or WebRTC functionality should depend on `getstream[webrtc]` instead of just `getstream`, and declare every other package they import themselves.
+5. **Rust bindings**: `getstream-rtc` (directory `getstream-rtc/`) is a separate distribution and a member of the uv workspace. Its package `getstream_rtc` contains the PyO3 extension `getstream_rtc._bindings`, which maturin builds from `getstream-rtc/Cargo.toml` (sources in `getstream-rtc/rustsrc/`), and exports every class at its root. It wraps the Rust SDK (`getstream` crate from https://github.com/GetStream/stream-video-rust). It does not depend on `getstream`; stream-py code imports `getstream_rtc` as a separate package, and only the `webrtc` extra installs it. Its version is independent of the `getstream` version and is only in `getstream-rtc/pyproject.toml` (the crate has no version). `uv sync` compiles it when it installs the `webrtc` extra or all workspace packages; this needs the Rust toolchain pinned in `getstream-rtc/rust-toolchain.toml`, a C compiler, `cmake`, `pkg-config`, `libvpx`, and libclang for bindgen (macOS: `brew install libvpx cmake pkg-config`, libclang comes with Xcode; Debian/Ubuntu: `apt install libvpx-dev libclang-dev cmake pkg-config build-essential`). After Rust changes, run `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings` and `cargo test --lib` in `getstream-rtc/`; rustup selects the toolchain from the current directory, not from `--manifest-path`.
## Python testing
@@ -17,3 +18,92 @@
8. tests that rely on file assets should always rely on files inside the `tests/assets/` folder, new files should be added there and existing ones used if possible. Do not use files larger than 256 kilobytes.
9. do not use mocks or mock things in general unless you are asked to do that directly
10. always run tests using `uv run pytest` from the root of the project, dont cd into folders to run tests
+
+
+## Rust and async expectations
+
+- Ensure [rust-skills](https://github.com/leonardomso/rust-skills) is available
+ before coding or review. Install or refresh it with
+ `npx add-skill leonardomso/rust-skills`, then apply the relevant rules rather
+ than loading unrelated categories.
+- Prefer typed errors and `Result`; avoid panics and `unwrap` in library code.
+- Do not circumvent borrow checker with .clone() for no reason.
+- Keep queues and buffers bounded. Make background-task cancellation and cleanup
+ deterministic, and do not hold synchronous locks across `.await` points.
+- Preserve compatibility with the Rust version declared in
+ `getstream-rtc/Cargo.toml`.
+
+## Rust wrapper: GIL, locks and deadlocks
+
+Python threads and tokio worker threads use SDK locks (`std::sync::Mutex`,
+tokio locks and channels) and the GIL. A deadlock needs a thread that holds an
+SDK lock and waits for the GIL while another thread holds the GIL and waits for
+that lock. Before you add or change a binding, read the SDK functions it calls
+and find the locks they take. Keep both halves impossible:
+
+- A Python thread must not hold the GIL while it waits for an SDK lock. Release
+ the GIL with `py.detach` in every synchronous method that reads or changes SDK
+ state (for example `Call.participants()`, `LocalAudioTrack.flush()`). A method
+ whose locks no other thread can hold yet needs no `py.detach` (`Client.call`
+ creates the call it locks).
+- A thread that holds an SDK lock must never wait for the GIL. Callbacks given
+ to the SDK (for example `on_track`) must not call `Python::attach` or drop
+ Python objects; they hand data over through a bounded channel.
+- `future_into_py` futures run on tokio without the GIL. Do not call
+ `Python::attach` in them; return the value and let pyo3-async-runtimes convert
+ it.
+- Never call Python from a `tracing` layer or a `log` logger. Records go
+ through the bounded queue in `getstream-rtc/rustsrc/logging.rs`. The Python
+ thread `getstream-logging` (`getstream-rtc/getstream_rtc/logging.py`) takes
+ them with `_next_logs()`, which waits without the GIL; Rust never calls
+ Python to log.
+- Python destructors drop the SDK objects they own with the GIL held. When you
+ update the SDK, check that these drops (`Call`, `RemoteTrack`,
+ `VideoFrameStream`, local tracks, tracks in the track queue) still take no
+ lock that another thread can hold.
+- While the GIL is released, do not read memory that Python owns (for example a
+ numpy buffer); copy it first.
+
+### Calling Python from Rust
+
+- Do not call Python code from Rust (`py.import(...)`, `call_method`, `call1`,
+ `getattr` of a Python object) unless the user agreed to it first. It makes the
+ code more complex and slower, so it is always a developer decision.
+- Return values that PyO3 converts by itself (`bool`, integers, `f64`, `String`,
+ `Vec`, `Option`, pyclasses) and let the Python caller convert them further,
+ for example a Unix time in seconds to a `datetime`.
+
+### Logging in the wrapper
+
+- Log with the `tracing` macros (`tracing::debug!` and the others). Do not use
+ `println!`, the `log` macros, or Python `logging` through `Python::attach`.
+- Use the default target. Records of the wrapper (`_bindings::…`) and of the SDK
+ (`getstream::…`) use the `level` of `configure_logging`; records of other
+ crates use `third_party_level`. A record reaches the configured logger's
+ child, for example `getstream._bindings.call` for
+ `getstream-rtc/rustsrc/call.rs` and `getstream.rtc.join` for the SDK's
+ `getstream::rtc::join`.
+- The message is an event name in the SDK style, `stream..`. Put
+ values in fields, not in the message:
+ `tracing::debug!(dropped_samples = n, "stream.rtc.audio.pcm_queue_overflow")`.
+- Each field becomes an attribute of the Python `LogRecord`. Dots in field names
+ become `_`, a name that `LogRecord` already has gets the prefix `rust_`, and
+ `rust_target` holds the Rust target. Booleans, integers, floats and strings
+ keep their type; other values become strings (`?value` for `Debug`, `%value`
+ for `Display`). An error field includes its source chain.
+- Levels map to Python as ERROR 40, WARN 30, INFO 20, DEBUG 10, TRACE 5.
+- A log call never blocks and never takes the GIL, so it is safe while a lock or
+ the GIL is held. When the queue (1024 records) is full, records are dropped and
+ counted; a `stream.rust.log_records_dropped` warning with `dropped` comes with
+ the next batch that the Python thread takes, before its records.
+- Nothing is forwarded until `configure_logging` sets a logger. Disabled
+ records are filtered before their fields are formatted. `configure_logging`
+ returns at once. In a process forked after the logging thread started,
+ `configure_logging` with a logger raises `RuntimeError`.
+- Never log secrets or tokens.
+- To assert on records in tests, including DEBUG ones, use the `sdk_logs`
+ fixture in `getstream-rtc/tests/conftest.py`, and call
+ `getstream_rtc.logging._flush()` before you assert: it returns after the
+ records queued before the call are delivered. Do not call it on a thread
+ that a handler waits for, such as an event loop that a handler runs
+ coroutines on; there, use `await asyncio.to_thread(getstream_rtc.logging._flush)`.
diff --git a/Makefile b/Makefile
index c4d97f70..492efd27 100644
--- a/Makefile
+++ b/Makefile
@@ -11,9 +11,8 @@ MARKER ?= not integration
VIDEO_PATHS := \
getstream/video \
tests/rtc \
+ getstream-rtc/tests \
tests/test_audio_stream_track.py \
- tests/test_connection_utils.py \
- tests/test_signaling.py \
tests/test_video_examples.py \
tests/test_video_integration.py \
tests/test_video_openai.py \
@@ -66,7 +65,7 @@ test-video:
## Run all tests (non-video + video), excluding manual tests
test-all:
- uv run pytest -m "$(MARKER)" tests/ getstream/ $(MANUAL_IGNORE)
+ uv run pytest -m "$(MARKER)" tests/ getstream/ getstream-rtc/tests $(MANUAL_IGNORE)
## Run integration tests for both groups
test-integration:
@@ -93,7 +92,7 @@ format:
## Run ty type checker
typecheck:
- uvx ty@0.0.24 check getstream/ $(TY_EXCLUDES)
+ uvx ty@0.0.24 check getstream/ --extra-search-path getstream-rtc $(TY_EXCLUDES)
## Run full check: lint + typecheck + non-video tests
check: lint typecheck test
diff --git a/examples/rtc/echo_agent.py b/examples/rtc/echo_agent.py
new file mode 100644
index 00000000..0c29976c
--- /dev/null
+++ b/examples/rtc/echo_agent.py
@@ -0,0 +1,156 @@
+"""Echo agent: joins a call, plays back what the person in the browser says,
+and shows their camera mirrored in its own video tile.
+
+It uses only the public `getstream.video.rtc` API: `rtc.join` with a
+subscription config, the `audio` and `track_added` events,
+`RemoteTrack.video_frames()`, `add_tracks` and `wait`.
+
+It reads the credentials (STREAM_API_KEY and STREAM_API_SECRET) from the
+environment or from the `.env` file of the repository:
+
+ uv run --extra webrtc python examples/rtc/echo_agent.py
+
+It opens the Stream demo app in the browser for a new call. Allow the
+microphone and the camera, then speak: the agent plays back your audio and
+shows your camera mirrored. Stop it with Ctrl+C. Use one person in the
+browser: the audio of several people is played back one after another, not
+mixed.
+"""
+
+import asyncio
+import contextlib
+import logging
+import uuid
+from fractions import Fraction
+from typing import Optional
+
+import aiortc
+import av
+import numpy as np
+from dotenv import load_dotenv
+
+from getstream import AsyncStream
+from getstream.models import CallRequest, UserRequest
+from getstream.video import rtc
+from getstream.video.rtc import (
+ AudioStreamTrack,
+ PcmData,
+ RemoteTrack,
+ TrackType,
+ VideoFrame,
+ VideoFrameStream,
+)
+from getstream.video.rtc.pb.stream.video.sfu.models.models_pb2 import (
+ TRACK_TYPE_AUDIO,
+ TRACK_TYPE_VIDEO,
+)
+from getstream.video.rtc.tracks import SubscriptionConfig, TrackSubscriptionConfig
+from getstream.video.rtc.utils import open_browser
+
+AGENT_ID = "echo-agent"
+HUMAN_ID = "echo-human"
+# The RTP clock of WebRTC video.
+VIDEO_TIME_BASE = Fraction(1, 90000)
+
+logger = logging.getLogger("echo_agent")
+
+
+def mirrored(frame: VideoFrame) -> av.VideoFrame:
+ """The frame flipped left to right, as a PyAV frame."""
+ width, height = frame.width, frame.height
+ # Packed I420: the Y plane, then the U and V planes at half size, rounded up.
+ chroma = ((height + 1) // 2, (width + 1) // 2)
+ image = av.VideoFrame(width, height, "yuv420p")
+ offset = 0
+ for plane, (rows, columns) in zip(image.planes, [(height, width), chroma, chroma]):
+ pixels = frame.data[offset : offset + rows * columns].reshape(rows, columns)
+ offset += rows * columns
+ # A PyAV plane can have padding at the end of each row.
+ padded = np.zeros((rows, plane.line_size), dtype=np.uint8)
+ padded[:, :columns] = pixels[:, ::-1]
+ plane.update(padded)
+ image.pts = frame.rtp_timestamp
+ image.time_base = VIDEO_TIME_BASE
+ return image
+
+
+class MirrorTrack(aiortc.MediaStreamTrack):
+ """A video track with the mirrored frames of the latest remote camera."""
+
+ kind = "video"
+
+ def __init__(self) -> None:
+ super().__init__()
+ self._frames: Optional[VideoFrameStream] = None
+ self._camera = asyncio.Event()
+
+ def show(self, frames: VideoFrameStream) -> None:
+ self._frames = frames
+ self._camera.set()
+
+ async def recv(self) -> av.VideoFrame:
+ while True:
+ await self._camera.wait()
+ frames = self._frames
+ assert frames is not None
+ try:
+ return mirrored(await anext(frames))
+ except StopAsyncIteration:
+ # The camera track ended; wait for the next one.
+ if self._frames is frames:
+ self._camera.clear()
+
+
+async def main() -> None:
+ async with AsyncStream() as client:
+ await client.upsert_users(UserRequest(id=AGENT_ID), UserRequest(id=HUMAN_ID))
+ call_id = str(uuid.uuid4())
+ call = client.video.call("default", call_id)
+ await call.get_or_create(data=CallRequest(created_by_id=AGENT_ID))
+
+ connection = await rtc.join(
+ call,
+ AGENT_ID,
+ subscription_config=SubscriptionConfig(
+ default=TrackSubscriptionConfig(
+ track_types=[TRACK_TYPE_AUDIO, TRACK_TYPE_VIDEO]
+ )
+ ),
+ )
+ audio = AudioStreamTrack()
+ mirror = MirrorTrack()
+
+ @connection.on("audio")
+ async def echo(pcm: PcmData) -> None:
+ await audio.write(pcm)
+
+ @connection.on("track_added")
+ def show_camera(track: RemoteTrack) -> None:
+ if track.track_type == TrackType.VIDEO:
+ # The stream keeps the track subscribed.
+ mirror.show(track.video_frames())
+
+ for event_name in (
+ "participant_joined",
+ "participant_left",
+ "track_published",
+ "track_unpublished",
+ ):
+ connection.on(event_name, lambda event: logger.info("%r", event))
+ connection.on(
+ "connection.state_changed",
+ lambda change: logger.info("state %s -> %s", change["old"], change["new"]),
+ )
+
+ async with connection:
+ await connection.add_tracks(audio=audio, video=mirror)
+ open_browser(client.api_key, client.create_token(HUMAN_ID), call_id)
+ # Returns when the call ends; Ctrl+C leaves the call.
+ await connection.wait()
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ load_dotenv()
+ with contextlib.suppress(KeyboardInterrupt):
+ asyncio.run(main())
diff --git a/examples/rtc/record_audio.py b/examples/rtc/record_audio.py
new file mode 100644
index 00000000..a666ed5b
--- /dev/null
+++ b/examples/rtc/record_audio.py
@@ -0,0 +1,94 @@
+"""Record audio: joins a call and writes the audio of each participant to a
+WAV file, one file for each participant session.
+
+It uses the public `getstream.video.rtc` API: `rtc.join` and the `audio`
+event, whose `PcmData` has the participant and 48 kHz mono samples.
+
+It reads the credentials (STREAM_API_KEY and STREAM_API_SECRET) from the
+environment or from the `.env` file of the repository:
+
+ uv run --extra webrtc python examples/rtc/record_audio.py
+
+It opens the Stream demo app in the browser for a new call. Allow the
+microphone and speak, then stop it with Ctrl+C. The files are written to the
+current directory as `_.wav`.
+
+Two limits:
+- No audio arrives while a participant is muted, so a file is shorter than
+ the call. The `pts` of each `PcmData` tells where to put silence.
+- The microphone and the screen-share audio of one participant go into the
+ same file.
+"""
+
+import asyncio
+import contextlib
+import logging
+import uuid
+import wave
+
+from dotenv import load_dotenv
+
+from getstream import AsyncStream
+from getstream.models import CallRequest, UserRequest
+from getstream.video import rtc
+from getstream.video.rtc import PcmData
+from getstream.video.rtc.utils import open_browser
+
+RECORDER_ID = "audio-recorder"
+SPEAKER_ID = "audio-speaker"
+
+logger = logging.getLogger("record_audio")
+
+
+class Recorder:
+ """Writes the audio of each participant session to its own WAV file."""
+
+ def __init__(self) -> None:
+ self._files: dict[str, wave.Wave_write] = {}
+
+ def write(self, pcm: PcmData) -> None:
+ participant = pcm.participant
+ file = self._files.get(participant.session_id)
+ if file is None:
+ path = f"{participant.user_id}_{participant.session_id}.wav"
+ file = wave.open(path, "wb")
+ file.setnchannels(pcm.channels)
+ file.setsampwidth(2) # 16-bit samples
+ file.setframerate(pcm.sample_rate)
+ self._files[participant.session_id] = file
+ logger.info("Recording %s", path)
+ file.writeframes(pcm.samples.tobytes())
+
+ def close(self) -> None:
+ for file in self._files.values():
+ file.close()
+
+
+async def main() -> None:
+ recorder = Recorder()
+ async with AsyncStream() as client:
+ await client.upsert_users(
+ UserRequest(id=RECORDER_ID), UserRequest(id=SPEAKER_ID)
+ )
+ call_id = str(uuid.uuid4())
+ call = client.video.call("default", call_id)
+ await call.get_or_create(data=CallRequest(created_by_id=RECORDER_ID))
+
+ # Remote audio arrives without a subscription config.
+ connection = await rtc.join(call, RECORDER_ID)
+ connection.on("audio", recorder.write)
+ try:
+ async with connection:
+ open_browser(client.api_key, client.create_token(SPEAKER_ID), call_id)
+ # Returns when the call ends; Ctrl+C leaves the call.
+ await connection.wait()
+ finally:
+ # The WAV header gets its length only when the file is closed.
+ recorder.close()
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ load_dotenv()
+ with contextlib.suppress(KeyboardInterrupt):
+ asyncio.run(main())
diff --git a/getstream-rtc/Cargo.lock b/getstream-rtc/Cargo.lock
new file mode 100644
index 00000000..80adcd72
--- /dev/null
+++ b/getstream-rtc/Cargo.lock
@@ -0,0 +1,4240 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common 0.1.7",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "arc-swap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "asn1-rs"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 1.0.69",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure 0.13.2",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "audio-codec-algorithms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1254ebf6529f3763c491acfb5ab6e960809e3c75a38584cc664f8c5667fb7107"
+
+[[package]]
+name = "audioadapter"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1292ef9edf681b7426ed089004b021a42896492f58bd0c909ad44e5faec9ac4"
+
+[[package]]
+name = "audioadapter-buffers"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46289a81a3bfa26d0f8b2415f8ca9decde9a66308a150568af5c102381b3c8ce"
+dependencies = [
+ "audioadapter",
+ "audioadapter-sample",
+ "num-traits",
+]
+
+[[package]]
+name = "audioadapter-sample"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5258faecf4edbe35ec483bda9b1f7438e6b391891819269c6b5461aeaa0e8d7d"
+dependencies = [
+ "audio-codec-algorithms",
+ "num-traits",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "beef"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
+
+[[package]]
+name = "bindgen"
+version = "0.68.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078"
+dependencies = [
+ "bitflags 2.13.2",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 1.1.0",
+ "shlex 1.3.0",
+ "syn 2.0.119",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytecheck"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26333eeac754f0ad8a6bcd0eb0ac012156302e4e16b852b72ee399aea4f12c29"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "rancor",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46d07918caa9eeaaf06b7873925c53a61daac173539b4f7715090745e44e4e69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex 2.0.1",
+]
+
+[[package]]
+name = "ccm"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847"
+dependencies = [
+ "aead",
+ "cipher",
+ "ctr",
+ "subtle",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+dependencies = [
+ "aead",
+ "chacha20 0.9.1",
+ "cipher",
+ "poly1305",
+ "zeroize",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common 0.1.7",
+ "inout",
+ "zeroize",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
+[[package]]
+name = "combine"
+version = "4.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core_detect"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "curve25519-dalek-derive",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid 0.9.6",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "der-parser"
+version = "9.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "const-oid 0.9.6",
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.1",
+ "const-oid 0.10.2",
+ "crypto-common 0.2.2",
+ "ctutils",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "dtls"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a431e87fc386bd5e02deb554a013f97bc406e47a7d8e97efb7c6366b980e9c"
+dependencies = [
+ "aes",
+ "aes-gcm",
+ "async-trait",
+ "bytecheck",
+ "byteorder",
+ "cbc",
+ "ccm",
+ "chacha20poly1305",
+ "der-parser",
+ "hmac 0.12.1",
+ "log",
+ "p256",
+ "p384",
+ "portable-atomic",
+ "rand 0.9.5",
+ "rand_core 0.6.4",
+ "rcgen",
+ "ring",
+ "rkyv",
+ "rustls",
+ "sec1",
+ "sha1 0.10.7",
+ "sha2 0.10.9",
+ "thiserror 1.0.69",
+ "tokio",
+ "webrtc-util",
+ "x25519-dalek",
+ "x509-parser",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest 0.10.7",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "either"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest 0.10.7",
+ "ff",
+ "generic-array",
+ "group",
+ "hkdf",
+ "pem-rfc7468",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
+dependencies = [
+ "cfg-if",
+ "core_detect",
+ "multiversion_no_op",
+ "rustversion",
+ "scopeguard",
+ "simdutf8",
+]
+
+[[package]]
+name = "env-libvpx-sys"
+version = "5.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26ecdc636a02003406cc821aa9d703c888a966a3fd9bbdae9f7cf27d71720147"
+dependencies = [
+ "bindgen",
+ "pkg-config",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getstream"
+version = "0.1.0-preview.2"
+source = "git+https://github.com/GetStream/stream-video-rust?rev=edd995b269471e53d40371a15dda007549bad6f8#edd995b269471e53d40371a15dda007549bad6f8"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "env-libvpx-sys",
+ "flate2",
+ "futures-util",
+ "hmac 0.13.0",
+ "indexmap",
+ "opus",
+ "prost",
+ "prost-build",
+ "prost-types",
+ "protox",
+ "reqwest",
+ "rubato",
+ "rustls",
+ "serde",
+ "serde_json",
+ "sha2 0.11.0",
+ "thiserror 2.0.21",
+ "tokio",
+ "tokio-tungstenite",
+ "tracing",
+ "url",
+ "uuid",
+ "webrtc",
+]
+
+[[package]]
+name = "getstream-rtc"
+version = "0.0.0"
+dependencies = [
+ "getstream",
+ "log",
+ "numpy",
+ "prost-types",
+ "pyo3",
+ "pyo3-async-runtimes",
+ "serde_json",
+ "tokio",
+ "tracing",
+ "tracing-log",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac 0.12.1",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "hyper"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2 0.6.5",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "interceptor"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88c11a956a48159f7fe539b8198f12b4db9b709ae5f94385b840db38f97fed74"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures",
+ "log",
+ "portable-atomic",
+ "rand 0.9.5",
+ "rtcp",
+ "rtp",
+ "thiserror 1.0.69",
+ "tokio",
+ "waitgroup",
+ "webrtc-srtp",
+ "webrtc-util",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.21",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "logos"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154"
+dependencies = [
+ "logos-derive 0.15.1",
+]
+
+[[package]]
+name = "logos"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f"
+dependencies = [
+ "logos-derive 0.16.1",
+]
+
+[[package]]
+name = "logos-codegen"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c"
+dependencies = [
+ "beef",
+ "fnv",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "regex-syntax",
+ "rustc_version",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "logos-codegen"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970"
+dependencies = [
+ "fnv",
+ "proc-macro2",
+ "quote",
+ "regex-automata",
+ "regex-syntax",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "logos-derive"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470"
+dependencies = [
+ "logos-codegen 0.15.1",
+]
+
+[[package]]
+name = "logos-derive"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31"
+dependencies = [
+ "logos-codegen 0.16.1",
+]
+
+[[package]]
+name = "lru-slab"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
+
+[[package]]
+name = "matrixmultiply"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
+dependencies = [
+ "autocfg",
+ "rawpointer",
+]
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memoffset"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miette"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
+dependencies = [
+ "cfg-if",
+ "miette-derive",
+ "unicode-width",
+]
+
+[[package]]
+name = "miette-derive"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "multimap"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+
+[[package]]
+name = "multiversion_no_op"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
+
+[[package]]
+name = "munge"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c"
+dependencies = [
+ "munge_macro",
+]
+
+[[package]]
+name = "munge_macro"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ndarray"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
+dependencies = [
+ "matrixmultiply",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rawpointer",
+]
+
+[[package]]
+name = "nix"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
+dependencies = [
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
+ "memoffset",
+ "pin-utils",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "numpy"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4"
+dependencies = [
+ "libc",
+ "ndarray",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "pyo3",
+ "pyo3-build-config",
+ "rustc-hash 2.1.3",
+]
+
+[[package]]
+name = "oid-registry"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
+dependencies = [
+ "asn1-rs",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "opus"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33718946cc77d4032911d4efe03a66dbcbfbd2bb16c3da06aaeadcc637c32216"
+dependencies = [
+ "opusic-sys",
+]
+
+[[package]]
+name = "opusic-sys"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9d1ecdf206421bc74343ab3bb2f30ad2abbfee41fa341f7181fecbaf957769a"
+dependencies = [
+ "cmake",
+]
+
+[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "p384"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64 0.22.1",
+ "serde_core",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "petgraph"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
+dependencies = [
+ "fixedbitset",
+ "hashbrown 0.15.5",
+ "indexmap",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
+dependencies = [
+ "heck",
+ "itertools",
+ "log",
+ "multimap",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 2.0.119",
+ "tempfile",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "prost-reflect"
+version = "0.16.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01b80ea363c31af2de2b92e3c07ed1156628f7838c4afb4df75ee78a37fedbd1"
+dependencies = [
+ "logos 0.16.1",
+ "miette",
+ "prost",
+ "prost-types",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "protox"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f25a07a73c6717f0b9bbbd685918f5df9815f7efba450b83d9c9dea41f0e3a1"
+dependencies = [
+ "bytes",
+ "miette",
+ "prost",
+ "prost-reflect",
+ "prost-types",
+ "protox-parse",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "protox-parse"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "072eee358134396a4643dff81cfff1c255c9fbd3fb296be14bdb6a26f9156366"
+dependencies = [
+ "logos 0.15.1",
+ "miette",
+ "prost-types",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.29.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
+dependencies = [
+ "libc",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+]
+
+[[package]]
+name = "pyo3-async-runtimes"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3ef68daa7316a3fac65e5e18b2203f010346de1c1c53456811a2624673ab046"
+dependencies = [
+ "futures-channel",
+ "futures-util",
+ "once_cell",
+ "pin-project-lite",
+ "pyo3",
+ "tokio",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.29.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
+dependencies = [
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.29.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.29.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.29.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash 2.1.3",
+ "rustls",
+ "socket2 0.6.5",
+ "thiserror 2.0.21",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand 0.10.3",
+ "rand_pcg",
+ "ring",
+ "rustc-hash 2.1.3",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.21",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2 0.6.5",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rancor"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b534442d0fcdb55d66f373d9cac6d33b6293a2335bc2136dbd06ce0e87d2572"
+dependencies = [
+ "ptr_meta",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
+dependencies = [
+ "chacha20 0.10.2",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
+[[package]]
+name = "rcgen"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
+dependencies = [
+ "pem",
+ "ring",
+ "rustls-pki-types",
+ "time",
+ "x509-parser",
+ "yasna",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rend"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac 0.12.1",
+ "subtle",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399"
+dependencies = [
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "munge",
+ "ptr_meta",
+ "rancor",
+ "rend",
+ "rkyv_derive",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "rtcp"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad7f6a501162881032fc84b4bc78ae11f1b30748180b9f5cbe8810bf613aab"
+dependencies = [
+ "bytes",
+ "thiserror 1.0.69",
+ "webrtc-util",
+]
+
+[[package]]
+name = "rtp"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "149329e78ada26b5e174a4c281a7a7e5c8bda6008adc90daddd6f98fce56db29"
+dependencies = [
+ "bytes",
+ "memchr",
+ "portable-atomic",
+ "rand 0.9.5",
+ "serde",
+ "thiserror 1.0.69",
+ "webrtc-util",
+]
+
+[[package]]
+name = "rubato"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc1e951b9f5432ec1422f4dcfa5733fc501265902612edf5ef8bd8ecdd960930"
+dependencies = [
+ "audioadapter",
+ "audioadapter-buffers",
+ "num-integer",
+ "num-traits",
+ "visibility",
+ "windowfunctions",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.13.2",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
+dependencies = [
+ "bitflags 2.13.2",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sdp"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b6eecfa5151edef84d544ff3885dff98e5b5fe97586757a4b5118bde51c958"
+dependencies = [
+ "rand 0.9.5",
+ "substring",
+ "thiserror 1.0.69",
+ "url",
+]
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha1"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest 0.10.7",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
+
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "stun"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e3b8d5ad1dd4c8cd0595440f26521a71dd593cb5873ee8e06b91510b7d97269"
+dependencies = [
+ "base64 0.22.1",
+ "crc",
+ "lazy_static",
+ "md-5",
+ "rand 0.9.5",
+ "ring",
+ "subtle",
+ "thiserror 1.0.69",
+ "tokio",
+ "url",
+ "webrtc-util",
+]
+
+[[package]]
+name = "substring"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "once_cell",
+ "rustix 1.1.5",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl 2.0.21",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.6.5",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite",
+ "webpki-roots 0.26.11",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "libc",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "sharded-slab",
+ "thread_local",
+ "tracing-core",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.10.3",
+ "rustls",
+ "rustls-pki-types",
+ "sha1 0.11.0",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "turn"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d99249a493335eb44c4d7943a8751b22561a82c9903d8eb2d29780b3927880a7"
+dependencies = [
+ "async-trait",
+ "base64 0.22.1",
+ "futures",
+ "log",
+ "md-5",
+ "portable-atomic",
+ "rand 0.9.5",
+ "ring",
+ "stun",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-util",
+ "webrtc-util",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "visibility"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "waitgroup"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292"
+dependencies = [
+ "atomic-waker",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
+dependencies = [
+ "js-sys",
+ "tokio",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.9",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webrtc"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baaacdf9d96224d7b6e2872ba065578f38775f7634367e3ac2cc87c7271da433"
+dependencies = [
+ "arc-swap",
+ "async-trait",
+ "bytes",
+ "dtls",
+ "hex",
+ "interceptor",
+ "lazy_static",
+ "log",
+ "portable-atomic",
+ "rand 0.9.5",
+ "rcgen",
+ "regex",
+ "ring",
+ "rtcp",
+ "rtp",
+ "sdp",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "smol_str",
+ "stun",
+ "thiserror 1.0.69",
+ "tokio",
+ "turn",
+ "unicase",
+ "url",
+ "waitgroup",
+ "webrtc-data",
+ "webrtc-ice",
+ "webrtc-mdns",
+ "webrtc-media",
+ "webrtc-sctp",
+ "webrtc-srtp",
+ "webrtc-util",
+]
+
+[[package]]
+name = "webrtc-data"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd470286275809f2fcfcdb1e73ef5f1500be82eff7fe98150ce81b20aad5a2a4"
+dependencies = [
+ "bytes",
+ "log",
+ "portable-atomic",
+ "thiserror 1.0.69",
+ "tokio",
+ "webrtc-sctp",
+ "webrtc-util",
+]
+
+[[package]]
+name = "webrtc-ice"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b7fd30f52e6fda8664779b84b7904b2553b76fee24d9ca665e774ae32b13f53"
+dependencies = [
+ "arc-swap",
+ "async-trait",
+ "crc",
+ "log",
+ "portable-atomic",
+ "rand 0.9.5",
+ "serde",
+ "serde_json",
+ "stun",
+ "thiserror 1.0.69",
+ "tokio",
+ "turn",
+ "url",
+ "uuid",
+ "waitgroup",
+ "webrtc-mdns",
+ "webrtc-util",
+]
+
+[[package]]
+name = "webrtc-mdns"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ffa0ea00c0fae979aafa5db285fec5eeb360e1ea246ebec529ba4e09f8e03e"
+dependencies = [
+ "log",
+ "socket2 0.5.10",
+ "thiserror 1.0.69",
+ "tokio",
+ "webrtc-util",
+]
+
+[[package]]
+name = "webrtc-media"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26a6c7335bdd03dc023cb9a3bd7966866d969c92c86987b73aa16dfa39c7ca29"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "rand 0.9.5",
+ "rtp",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "webrtc-sctp"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c4b637f0d8eb96d900ac0f79b3060ddd21ca88dfefa467e96e67ab0016f2574"
+dependencies = [
+ "arc-swap",
+ "async-trait",
+ "bytes",
+ "crc",
+ "log",
+ "portable-atomic",
+ "rand 0.9.5",
+ "thiserror 1.0.69",
+ "tokio",
+ "webrtc-util",
+]
+
+[[package]]
+name = "webrtc-srtp"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45d2b667a0b5d04eebcb7cb22fd51b2af3721f418097b496e72de16386c8d0c3"
+dependencies = [
+ "aead",
+ "aes",
+ "aes-gcm",
+ "byteorder",
+ "bytes",
+ "ctr",
+ "hmac 0.12.1",
+ "log",
+ "rtcp",
+ "rtp",
+ "sha1 0.10.7",
+ "subtle",
+ "thiserror 1.0.69",
+ "tokio",
+ "webrtc-util",
+]
+
+[[package]]
+name = "webrtc-util"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1beae0b4f24969741c26ca282a257dc5823bd9cbe608f7e3ca3775102d323ad9"
+dependencies = [
+ "async-trait",
+ "bitflags 1.3.2",
+ "bytes",
+ "ipnet",
+ "lazy_static",
+ "log",
+ "nix",
+ "portable-atomic",
+ "rand 0.9.5",
+ "thiserror 1.0.69",
+ "tokio",
+ "winapi",
+]
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix 0.38.44",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windowfunctions"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90628d739333b7c5d2ee0b70210b97b8cddc38440c682c96fd9e2c24c2db5f3a"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "x25519-dalek"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
+dependencies = [
+ "curve25519-dalek",
+ "rand_core 0.6.4",
+ "serde",
+ "zeroize",
+]
+
+[[package]]
+name = "x509-parser"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom",
+ "oid-registry",
+ "ring",
+ "rusticata-macros",
+ "thiserror 1.0.69",
+ "time",
+]
+
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+ "synstructure 0.14.0",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+ "synstructure 0.14.0",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/getstream-rtc/Cargo.toml b/getstream-rtc/Cargo.toml
new file mode 100644
index 00000000..6d2e5f09
--- /dev/null
+++ b/getstream-rtc/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "getstream-rtc"
+edition = "2024"
+rust-version = "1.88"
+publish = false
+
+[lib]
+name = "_bindings"
+path = "rustsrc/lib.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+getstream = { git = "https://github.com/GetStream/stream-video-rust", rev = "edd995b269471e53d40371a15dda007549bad6f8" }
+log = "0.4"
+numpy = "0.29"
+prost-types = "0.14"
+serde_json = { version = "1", features = ["float_roundtrip", "preserve_order"] }
+pyo3 = { version = "0.29.2", features = ["abi3-py310"] }
+pyo3-async-runtimes = { version = "0.29.0", features = ["tokio-runtime"] }
+tokio = { version = "1.53.1", features = ["macros", "rt", "sync"] }
+tracing = "0.1"
+tracing-log = { version = "0.2", default-features = false, features = ["log-tracer", "std"] }
+tracing-subscriber = { version = "0.3", default-features = false, features = ["registry", "std"] }
diff --git a/getstream-rtc/getstream_rtc/__init__.py b/getstream-rtc/getstream_rtc/__init__.py
new file mode 100644
index 00000000..b59ca3a0
--- /dev/null
+++ b/getstream-rtc/getstream_rtc/__init__.py
@@ -0,0 +1,108 @@
+import importlib.metadata
+
+from getstream_rtc.errors import (
+ ApiError,
+ ConfigError,
+ CoordinatorError,
+ IllegalStateError,
+ MediaError,
+ PcmQueueOverflowError,
+ PcmRateMismatchError,
+ PermissionDeniedError,
+ RtcError,
+ RustError,
+)
+from getstream_rtc._bindings import (
+ AudioLevelChanged,
+ Call,
+ CallEnded,
+ CallEvent,
+ CallGrantsUpdated,
+ CallingState,
+ CallingStateChanged,
+ ChangePublishOptions,
+ ChangePublishQuality,
+ Client,
+ ConnectionQualityChanged,
+ CoordinatorEvent,
+ DominantSpeakerChanged,
+ Error,
+ EventStream,
+ EventsLagged,
+ ICERestart,
+ InboundStateNotification,
+ LocalAudioTrack,
+ LocalVideoTrack,
+ ParticipantCountChanged,
+ ParticipantJoined,
+ ParticipantLeft,
+ ParticipantUpdated,
+ PcmFrame,
+ PinsChanged,
+ RemoteParticipant,
+ RemoteTrack,
+ StreamResampler,
+ SubscriptionConfig,
+ TrackPublished,
+ TrackStream,
+ TrackSubscriptionConfig,
+ TrackType,
+ TrackUnpublished,
+ VideoFrame,
+ VideoFrameStream,
+)
+from getstream_rtc.logging import configure_logging
+
+__version__ = importlib.metadata.version("getstream-rtc")
+
+__all__ = [
+ "ApiError",
+ "AudioLevelChanged",
+ "Call",
+ "CallEnded",
+ "CallEvent",
+ "CallGrantsUpdated",
+ "CallingState",
+ "CallingStateChanged",
+ "ChangePublishOptions",
+ "ChangePublishQuality",
+ "Client",
+ "ConfigError",
+ "ConnectionQualityChanged",
+ "CoordinatorError",
+ "CoordinatorEvent",
+ "DominantSpeakerChanged",
+ "Error",
+ "EventStream",
+ "EventsLagged",
+ "ICERestart",
+ "IllegalStateError",
+ "InboundStateNotification",
+ "LocalAudioTrack",
+ "LocalVideoTrack",
+ "MediaError",
+ "ParticipantCountChanged",
+ "ParticipantJoined",
+ "ParticipantLeft",
+ "ParticipantUpdated",
+ "PcmFrame",
+ "PcmQueueOverflowError",
+ "PcmRateMismatchError",
+ "PermissionDeniedError",
+ "PinsChanged",
+ "RemoteParticipant",
+ "RemoteTrack",
+ "RtcError",
+ "RustError",
+ "StreamResampler",
+ "SubscriptionConfig",
+ "TrackPublished",
+ "TrackStream",
+ "TrackSubscriptionConfig",
+ "TrackType",
+ "TrackUnpublished",
+ "VideoFrame",
+ "VideoFrameStream",
+ "__version__",
+ "configure_logging",
+]
diff --git a/getstream-rtc/getstream_rtc/_bindings.pyi b/getstream-rtc/getstream_rtc/_bindings.pyi
new file mode 100644
index 00000000..f722d2f9
--- /dev/null
+++ b/getstream-rtc/getstream_rtc/_bindings.pyi
@@ -0,0 +1,573 @@
+from enum import Enum
+from typing import Any, Awaitable
+
+import numpy as np
+import numpy.typing as npt
+
+def _set_log_filters(level: int | None, third_party_level: int) -> None:
+ """Queue SDK records at `level` or above, and third-party records at the
+ less verbose of `level` and `third_party_level`; `level=None` queues
+ none."""
+
+def _next_logs() -> tuple[
+ int,
+ list[tuple[str | None, int, str, int, str, int, str, dict[str, object]]],
+ int | None,
+]:
+ """Wait without the GIL for queued records. Return the number of records
+ dropped since the last call, at most 256 records as the arguments of
+ `getstream_rtc.logging.handle_record` after its logger, and the marker that
+ ended the batch, if any."""
+
+def _push_log_marker(marker: int) -> None:
+ """Put `marker` into the queue after the queued records; wait without the
+ GIL while the queue is full."""
+
+class CallingState(Enum):
+ IDLE = ...
+ JOINING = ...
+ JOINED = ...
+ RECONNECTING = ...
+ MIGRATING = ...
+ RECONNECTING_FAILED = ...
+ LEFT = ...
+
+class TrackType(Enum):
+ UNSPECIFIED = ...
+ AUDIO = ...
+ VIDEO = ...
+ SCREEN_SHARE = ...
+ SCREEN_SHARE_AUDIO = ...
+
+class RemoteParticipant:
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+ @property
+ def name(self) -> str: ...
+ @property
+ def image(self) -> str: ...
+ @property
+ def roles(self) -> list[str]: ...
+ @property
+ def published_tracks(self) -> list[TrackType]: ...
+ @property
+ def connection_quality(self) -> int:
+ """The protobuf ``ConnectionQuality`` value."""
+ @property
+ def is_speaking(self) -> bool: ...
+ @property
+ def is_dominant_speaker(self) -> bool: ...
+ @property
+ def audio_level(self) -> float: ...
+ @property
+ def source(self) -> int:
+ """The protobuf ``ParticipantSource`` value."""
+ @property
+ def joined_at(self) -> float | None:
+ """Unix time in seconds, or ``None`` if the SFU did not send it."""
+ @property
+ def custom(self) -> dict[str, Any]:
+ """Empty if the SFU did not send it."""
+
+class TrackSubscriptionConfig:
+ """The subscription rule for the participants of one role, or the default
+ rule. A ``None`` argument keeps the SDK default (no track types,
+ 1920x1080)."""
+
+ def __init__(
+ self,
+ track_types: list[TrackType] | None = None,
+ video_dimension: tuple[int, int] | None = None,
+ screenshare_dimension: tuple[int, int] | None = None,
+ ) -> None: ...
+ @property
+ def track_types(self) -> list[TrackType]: ...
+ @property
+ def video_dimension(self) -> tuple[int, int]: ...
+ @property
+ def screenshare_dimension(self) -> tuple[int, int]: ...
+
+class SubscriptionConfig:
+ """The SDK applies the rule of the first role in ``participant.roles`` that
+ has an entry in ``role_filters``, or else ``default``, and keeps at most
+ ``max_subscriptions`` tracks."""
+
+ def __init__(
+ self,
+ default: TrackSubscriptionConfig | None = None,
+ role_filters: dict[str, TrackSubscriptionConfig] | None = None,
+ max_subscriptions: int | None = None,
+ ) -> None: ...
+ @property
+ def default(self) -> TrackSubscriptionConfig: ...
+ @property
+ def role_filters(self) -> dict[str, TrackSubscriptionConfig]: ...
+ @property
+ def max_subscriptions(self) -> int | None: ...
+
+class CallStateSnapshot:
+ @property
+ def participants(self) -> list[RemoteParticipant]: ...
+ @property
+ def own_capabilities(self) -> list[str]: ...
+
+class CallEvent:
+ @property
+ def name(self) -> str:
+ """The stable SDK event name: the ``SfuEvent`` oneof field name for SFU
+ events, the ``type`` for coordinator events."""
+
+class ParticipantJoined(CallEvent):
+ @property
+ def call_cid(self) -> str: ...
+ @property
+ def participant(self) -> RemoteParticipant: ...
+
+class ParticipantLeft(CallEvent):
+ @property
+ def call_cid(self) -> str: ...
+ @property
+ def participant(self) -> RemoteParticipant: ...
+
+class TrackPublished(CallEvent):
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+ @property
+ def track_type(self) -> TrackType: ...
+ @property
+ def participant(self) -> RemoteParticipant | None: ...
+
+class TrackUnpublished(CallEvent):
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+ @property
+ def track_type(self) -> TrackType: ...
+ @property
+ def cause(self) -> int:
+ """The protobuf ``TrackUnpublishReason`` value."""
+ @property
+ def participant(self) -> RemoteParticipant | None: ...
+
+class CallEnded(CallEvent):
+ def __init__(self) -> None:
+ """For an end that comes without the SFU ``call_ended``; its ``reason``
+ is 0 (``CALL_ENDED_REASON_UNSPECIFIED``)."""
+ @property
+ def reason(self) -> int:
+ """The protobuf ``CallEndedReason`` value."""
+
+class CallingStateChanged(CallEvent):
+ @property
+ def state(self) -> CallingState: ...
+
+class CoordinatorEvent(CallEvent):
+ """A coordinator event of this call; ``name`` is its JSON ``type``."""
+
+ @property
+ def data(self) -> dict[str, Any]:
+ """The whole JSON message."""
+
+class ParticipantUpdated(CallEvent):
+ @property
+ def call_cid(self) -> str: ...
+ @property
+ def participant(self) -> RemoteParticipant: ...
+
+class DominantSpeakerChanged(CallEvent):
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+
+class AudioLevel:
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+ @property
+ def level(self) -> float:
+ """0.0 is silence, 1.0 the loudest."""
+ @property
+ def is_speaking(self) -> bool: ...
+
+class AudioLevelChanged(CallEvent):
+ @property
+ def audio_levels(self) -> list[AudioLevel]: ...
+
+class ConnectionQualityInfo:
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+ @property
+ def connection_quality(self) -> int:
+ """The protobuf ``ConnectionQuality`` value."""
+
+class ConnectionQualityChanged(CallEvent):
+ @property
+ def connection_quality_updates(self) -> list[ConnectionQualityInfo]: ...
+
+class ParticipantCountChanged(CallEvent):
+ """Sent by the SDK when the SFU's participant count changes."""
+
+ @property
+ def total(self) -> int: ...
+ @property
+ def anonymous(self) -> int: ...
+
+class Pin:
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+
+class PinsChanged(CallEvent):
+ @property
+ def pins(self) -> list[Pin]: ...
+
+class InboundVideoState:
+ @property
+ def user_id(self) -> str: ...
+ @property
+ def session_id(self) -> str: ...
+ @property
+ def track_type(self) -> TrackType: ...
+ @property
+ def paused(self) -> bool: ...
+
+class InboundStateNotification(CallEvent):
+ @property
+ def inbound_video_states(self) -> list[InboundVideoState]: ...
+
+class CallGrants:
+ @property
+ def can_publish_audio(self) -> bool: ...
+ @property
+ def can_publish_video(self) -> bool: ...
+ @property
+ def can_screenshare(self) -> bool: ...
+
+class CallGrantsUpdated(CallEvent):
+ @property
+ def current_grants(self) -> CallGrants | None: ...
+ @property
+ def message(self) -> str: ...
+
+class ICERestart(CallEvent):
+ """Sent after the SDK has handled the SFU's ICE restart request."""
+
+ @property
+ def peer_type(self) -> int:
+ """The protobuf ``PeerType`` value."""
+
+class Codec:
+ @property
+ def payload_type(self) -> int: ...
+ @property
+ def name(self) -> str: ...
+ @property
+ def clock_rate(self) -> int: ...
+ @property
+ def encoding_parameters(self) -> str: ...
+ @property
+ def fmtp(self) -> str: ...
+
+class VideoDimension:
+ @property
+ def width(self) -> int: ...
+ @property
+ def height(self) -> int: ...
+
+class AudioBitrate:
+ @property
+ def profile(self) -> int:
+ """The protobuf ``AudioBitrateProfile`` value."""
+ @property
+ def bitrate(self) -> int: ...
+
+class PublishOption:
+ @property
+ def track_type(self) -> TrackType: ...
+ @property
+ def codec(self) -> Codec | None: ...
+ @property
+ def bitrate(self) -> int: ...
+ @property
+ def fps(self) -> int: ...
+ @property
+ def max_spatial_layers(self) -> int: ...
+ @property
+ def max_temporal_layers(self) -> int: ...
+ @property
+ def video_dimension(self) -> VideoDimension | None: ...
+ @property
+ def id(self) -> int: ...
+ @property
+ def use_single_layer(self) -> bool: ...
+ @property
+ def audio_bitrate_profiles(self) -> list[AudioBitrate]: ...
+ @property
+ def degradation_preference(self) -> int:
+ """The protobuf ``DegradationPreference`` value."""
+
+class ChangePublishOptions(CallEvent):
+ @property
+ def publish_options(self) -> list[PublishOption]: ...
+ @property
+ def reason(self) -> str: ...
+
+class AudioSender:
+ @property
+ def codec(self) -> Codec | None: ...
+ @property
+ def track_type(self) -> TrackType: ...
+ @property
+ def publish_option_id(self) -> int: ...
+
+class VideoLayerSetting:
+ @property
+ def name(self) -> str: ...
+ @property
+ def active(self) -> bool: ...
+ @property
+ def max_bitrate(self) -> int: ...
+ @property
+ def scale_resolution_down_by(self) -> float: ...
+ @property
+ def codec(self) -> Codec | None: ...
+ @property
+ def max_framerate(self) -> int: ...
+ @property
+ def scalability_mode(self) -> str: ...
+
+class VideoSender:
+ @property
+ def codec(self) -> Codec | None: ...
+ @property
+ def layers(self) -> list[VideoLayerSetting]: ...
+ @property
+ def track_type(self) -> TrackType: ...
+ @property
+ def publish_option_id(self) -> int: ...
+ @property
+ def degradation_preference(self) -> int:
+ """The protobuf ``DegradationPreference`` value."""
+
+class ChangePublishQuality(CallEvent):
+ @property
+ def audio_senders(self) -> list[AudioSender]: ...
+ @property
+ def video_senders(self) -> list[VideoSender]: ...
+
+class ErrorDetails:
+ """The protobuf ``models.Error`` inside the ``Error`` event."""
+
+ @property
+ def code(self) -> int:
+ """The protobuf ``ErrorCode`` value."""
+ @property
+ def message(self) -> str: ...
+ @property
+ def should_retry(self) -> bool: ...
+
+class EventsLagged(CallEvent):
+ """Sent by the wrapper when this stream fell behind and lost ``skipped``
+ events; the stream continues with the oldest event still buffered."""
+
+ @property
+ def skipped(self) -> int: ...
+
+class Error(CallEvent):
+ @property
+ def error(self) -> ErrorDetails: ...
+ @property
+ def reconnect_strategy(self) -> int:
+ """The protobuf ``WebsocketReconnectStrategy`` value."""
+
+Event = (
+ ParticipantJoined
+ | ParticipantLeft
+ | TrackPublished
+ | TrackUnpublished
+ | CallEnded
+ | ParticipantUpdated
+ | DominantSpeakerChanged
+ | AudioLevelChanged
+ | ConnectionQualityChanged
+ | ParticipantCountChanged
+ | PinsChanged
+ | InboundStateNotification
+ | CallGrantsUpdated
+ | ICERestart
+ | ChangePublishOptions
+ | ChangePublishQuality
+ | Error
+ | EventsLagged
+ | CallingStateChanged
+ | CoordinatorEvent
+)
+
+class EventStream:
+ """Ends when the call ends (``CallingStateChanged(LEFT)``), after the events
+ that are already buffered, or at once if the call had already ended when
+ the stream was created."""
+
+ def __aiter__(self) -> EventStream: ...
+ def __anext__(self) -> Awaitable[Event]: ...
+
+class PcmFrame:
+ @property
+ def samples(self) -> npt.NDArray[np.int16]: ...
+ @property
+ def sample_rate(self) -> int: ...
+ @property
+ def channels(self) -> int: ...
+ @property
+ def pts(self) -> int | None:
+ """The RTP timestamp of the first sample, on the 48 kHz Opus clock; it
+ wraps at 2**32."""
+
+class VideoFrame:
+ @property
+ def width(self) -> int: ...
+ @property
+ def height(self) -> int: ...
+ @property
+ def data(self) -> npt.NDArray[np.uint8]: ...
+ @property
+ def rtp_timestamp(self) -> int: ...
+
+class VideoFrameStream:
+ """Decoded frames of one video track. Each ``RemoteTrack.video_frames()``
+ call gives an independent stream with its own copy of each frame. A stream
+ that reads slower than the track gets the latest frame and skips the older
+ ones. Ends when the track ends, or at once for a track with no video
+ decoder."""
+
+ def __aiter__(self) -> VideoFrameStream: ...
+ def __anext__(self) -> Awaitable[VideoFrame]: ...
+
+class RemoteTrack:
+ @property
+ def participant(self) -> RemoteParticipant: ...
+ @property
+ def track_type(self) -> TrackType: ...
+ def next_pcm(self) -> Awaitable[PcmFrame | None]: ...
+ def video_frames(self) -> VideoFrameStream:
+ """A new stream of the frames decoded after this call. The track stays
+ subscribed while one of its streams exists."""
+
+class TrackStream:
+ """All streams of one call share one queue. Ends when the call ends, or at
+ once if the call had already ended when the stream was created."""
+
+ def __aiter__(self) -> TrackStream: ...
+ def __anext__(self) -> Awaitable[RemoteTrack]: ...
+
+class LocalAudioTrack:
+ def __init__(self, pcm_queue_capacity: float | None = None) -> None:
+ """``pcm_queue_capacity``: the seconds of PCM that ``write_pcm`` queues
+ (``None``: the SDK default, 60 s; below 0.02 raises ``MediaError``). A
+ write above it drops the oldest samples and raises
+ ``PcmQueueOverflowError``."""
+ def write_pcm(
+ self, samples: npt.NDArray[np.int16], sample_rate: int, channels: int
+ ) -> Awaitable[None]:
+ """Await each write before the next; writes that run at the same time
+ can reach the SDK out of order. The first frame sets the sample rate of
+ the track; a frame at another rate raises ``PcmRateMismatchError``."""
+ def flush(self) -> None: ...
+
+class StreamResampler:
+ """Resamples one stream of interleaved int16 PCM to ``sample_rate`` with a
+ windowed-sinc filter, without a change of the channel count. The filter
+ state carries over between calls. The filter delays the audio by 128 input
+ frames (8 ms at 16 kHz) and holds them until the next ``push`` or
+ ``flush``."""
+
+ def __init__(self, sample_rate: int, channels: int) -> None: ...
+ def push(
+ self, samples: npt.NDArray[np.int16], sample_rate: int, channels: int
+ ) -> PcmFrame:
+ """The first call sets the input rate. A later frame at another rate
+ raises ``PcmRateMismatchError``, a frame with another channel count
+ ``IllegalStateError``. Input at the output rate is copied."""
+ def flush(self) -> PcmFrame:
+ """Returns the audio that the filter holds, followed by near-silence
+ (about 132 input frames). Returns an empty frame when nothing was pushed
+ since the last flush, or when the input is at the output rate. Later
+ pushes continue at the same input rate."""
+
+class LocalVideoTrack:
+ @staticmethod
+ def vp8() -> LocalVideoTrack: ...
+ @staticmethod
+ def vp9() -> LocalVideoTrack: ...
+ def write_i420(
+ self, data: npt.NDArray[np.uint8], width: int, height: int, duration: float
+ ) -> Awaitable[None]:
+ """Await each write before the next; writes that run at the same time
+ can reach the SDK out of order."""
+
+class Call:
+ def join(self, user_id: str, create: bool = True) -> Awaitable[None]: ...
+ def leave(self) -> Awaitable[None]: ...
+ def session_id(self) -> Awaitable[str | None]: ...
+ @property
+ def calling_state(self) -> CallingState: ...
+ def participants(self) -> list[RemoteParticipant]: ...
+ def call_state(self) -> CallStateSnapshot: ...
+ def sfu_events(self) -> EventStream:
+ """SFU events. The SFU ``call_ended`` may not come: the SDK also leaves
+ on the coordinator ``call.ended``."""
+ def coordinator_events(self) -> EventStream:
+ """``CoordinatorEvent`` for each coordinator event of this call."""
+ def client_events(self) -> EventStream:
+ """``CallingStateChanged``; ``LEFT`` is the reliable end of the call."""
+ def tracks(self) -> TrackStream: ...
+ def publish_audio(self, track: LocalAudioTrack) -> Awaitable[None]: ...
+ def publish_video(self, track: LocalVideoTrack) -> Awaitable[None]: ...
+ def publish_screen_share(self, track: LocalVideoTrack) -> Awaitable[None]:
+ """The SFU accepts VP8 for screen share (``LocalVideoTrack.vp8()``)."""
+ def stop_publish_audio(self, track: LocalAudioTrack) -> Awaitable[None]:
+ """Stops the track: later writes raise ``IllegalStateError``. A track
+ that is not published is ignored."""
+ def stop_publish_video(self, track: LocalVideoTrack) -> Awaitable[None]:
+ """Stops the track: later writes raise ``IllegalStateError``. A track
+ that is not published is ignored."""
+ def stop_publish_screen_share(self, track: LocalVideoTrack) -> Awaitable[None]:
+ """Stops the track: later writes raise ``IllegalStateError``. A track
+ that is not published is ignored."""
+ def mute_track(self, track_type: TrackType) -> Awaitable[None]:
+ """Keeps the published track; others get ``TrackUnpublished``."""
+ def unmute_track(self, track_type: TrackType) -> Awaitable[None]:
+ """Others get ``TrackPublished``; their track delivers frames again."""
+ def update_subscriptions(self, config: SubscriptionConfig) -> Awaitable[None]:
+ """The SDK applies ``config`` again on each participant and track event.
+ Remote audio can arrive without a subscription: the SFU sends it."""
+
+class Client:
+ def __init__(
+ self,
+ api_key: str,
+ api_secret: str | None = None,
+ *,
+ token: str | None = None,
+ base_url: str | None = None,
+ ws_url: str | None = None,
+ log_bodies: bool = False,
+ call_event_capacity: int | None = None,
+ ) -> None:
+ """Pass exactly one of ``api_secret`` or ``token`` (a user token); else
+ ``ConfigError``. A token client joins only as the token's user.
+
+ ``call_event_capacity``: the events that each event stream of a call
+ buffers for a reader that falls behind (``None``: the SDK default, 256);
+ 0 raises ``ConfigError``."""
+ def call(self, call_type: str, call_id: str) -> Call: ...
diff --git a/getstream-rtc/getstream_rtc/errors.py b/getstream-rtc/getstream_rtc/errors.py
new file mode 100644
index 00000000..dd99e54b
--- /dev/null
+++ b/getstream-rtc/getstream_rtc/errors.py
@@ -0,0 +1,71 @@
+class RustError(Exception):
+ """Base class of the errors that ``getstream_rtc`` raises."""
+
+
+class ConfigError(RustError):
+ """The client configuration is invalid."""
+
+
+class RtcError(RustError):
+ """An RTC operation of the Rust SDK failed."""
+
+
+class ApiError(RtcError):
+ """The Stream API returned an error response. ``message`` is the message
+ of the API; ``str(error)`` also contains the code and the HTTP status."""
+
+ def __init__(
+ self,
+ text: str,
+ code: int,
+ status_code: int,
+ message: str,
+ unrecoverable: bool,
+ ):
+ super().__init__(text)
+ self.code = code
+ self.status_code = status_code
+ self.message = message
+ self.unrecoverable = unrecoverable
+
+
+class CoordinatorError(RtcError):
+ """A coordinator step of the join failed, for example the coordinator
+ rejected the credentials."""
+
+
+class IllegalStateError(RtcError):
+ """The operation is not valid in the current state of the call or track."""
+
+
+class MediaError(RtcError):
+ """Encoding, decoding or packetizing media failed, or a frame is invalid."""
+
+
+class PermissionDeniedError(RtcError):
+ """The participant lacks ``capability`` (for example ``send-audio``), which
+ the operation requires."""
+
+ def __init__(self, message: str, capability: str):
+ super().__init__(message)
+ self.capability = capability
+
+
+class PcmQueueOverflowError(RtcError):
+ """A PCM write exceeded the queue capacity. The newest samples were kept
+ and ``dropped_samples`` oldest samples were discarded."""
+
+ def __init__(self, message: str, dropped_samples: int, capacity_samples: int):
+ super().__init__(message)
+ self.dropped_samples = dropped_samples
+ self.capacity_samples = capacity_samples
+
+
+class PcmRateMismatchError(RtcError):
+ """A PCM frame is not at the sample rate ``expected`` of the first frame;
+ ``actual`` is its rate. The frame was not used."""
+
+ def __init__(self, message: str, expected: int, actual: int):
+ super().__init__(message)
+ self.expected = expected
+ self.actual = actual
diff --git a/getstream-rtc/getstream_rtc/logging.py b/getstream-rtc/getstream_rtc/logging.py
new file mode 100644
index 00000000..750102b3
--- /dev/null
+++ b/getstream-rtc/getstream_rtc/logging.py
@@ -0,0 +1,163 @@
+"""Hands the records that Rust queues to Python `logging`.
+
+Rust only queues records. The thread `getstream-logging` takes them with
+`_bindings._next_logs()`, which waits without the GIL, and hands them to the
+configured logger. Rust never calls Python to log.
+"""
+
+import atexit
+import itertools
+import logging
+import os
+import threading
+import traceback
+
+from getstream_rtc import _bindings
+
+_lock = threading.Lock()
+_logger: logging.Logger | None = None
+_thread: threading.Thread | None = None
+# The process that started `_thread`. A child of fork() has no `_thread`. It
+# changes only once, from `None`, so it is read without `_lock`: a child forked
+# while `_lock` was held cannot take it.
+_pid: int | None = None
+_stop_marker: int | None = None
+_markers: dict[int, threading.Event] = {}
+_marker_ids = itertools.count()
+
+
+def configure_logging(
+ logger: logging.Logger | None,
+ level: int,
+ third_party_level: int = logging.WARNING,
+) -> None:
+ """Send SDK records at `level` or above, and records of third-party Rust
+ crates at the less verbose of `level` and `third_party_level`, to `logger`
+ and the loggers below it. `logger=None` stops forwarding. Process-wide: the
+ last call wins. Returns at once: the thread `getstream-logging` delivers
+ the records.
+
+ Raises `RuntimeError` for a `logger` in a process forked after the logging
+ was configured: the child has no thread of the parent. Configure the
+ logging and use `getstream_rtc` only after fork()."""
+ global _logger, _thread, _pid
+ if _pid is not None and _pid != os.getpid():
+ if logger is None:
+ return
+ raise RuntimeError(
+ "the logging of getstream_rtc was configured before this process "
+ "was forked: configure the logging and use getstream_rtc only "
+ "after fork()"
+ )
+ with _lock:
+ if _stop_marker is not None:
+ return
+ _bindings._set_log_filters(None if logger is None else level, third_party_level)
+ _logger = logger
+ if logger is not None and (_thread is None or not _thread.is_alive()):
+ _thread = threading.Thread(
+ target=_forward, name="getstream-logging", daemon=True
+ )
+ _thread.start()
+ _pid = os.getpid()
+
+
+def handle_record(
+ base: logging.Logger,
+ name: str | None,
+ level: int,
+ pathname: str,
+ lineno: int,
+ msg: str,
+ created_ns: int,
+ rust_target: str,
+ fields: dict[str, object],
+) -> None:
+ logger = base.getChild(name) if name else base
+ if not logger.isEnabledFor(level):
+ return
+ record = logger.makeRecord(logger.name, level, pathname, lineno, msg, (), None)
+ for key, value in fields.items():
+ setattr(record, f"rust_{key}" if hasattr(record, key) else key, value)
+ record.rust_target = rust_target
+ # The time of the Rust event, not of this call. LogRecord derives msecs
+ # and relativeCreated from the same clock reading as created.
+ created = created_ns / 1e9
+ record.relativeCreated -= (record.created - created) * 1000
+ record.created = created
+ record.msecs = (created_ns % 1_000_000_000) // 1_000_000 + 0.0
+ logger.handle(record)
+
+
+def _forward() -> None:
+ while True:
+ dropped, records, marker = _bindings._next_logs()
+ logger = _logger
+ if logger is not None:
+ # An error of one record is printed, as `logging.Handler.handleError`
+ # does, and the thread goes on: a `_flush()` that waits for a
+ # marker in the queue needs it.
+ if dropped:
+ try:
+ logger.warning(
+ "stream.rust.log_records_dropped", extra={"dropped": dropped}
+ )
+ except Exception:
+ traceback.print_exc()
+ for record in records:
+ try:
+ handle_record(logger, *record)
+ except Exception:
+ traceback.print_exc()
+ if marker is not None:
+ _markers.pop(marker).set()
+ if marker == _stop_marker:
+ return
+
+
+def _flush() -> None:
+ """Return after the records queued before this call are handled.
+
+ Returns at once on the thread `getstream-logging` itself, when that thread
+ does not run or is stopping, and in a child of fork(). Do not call it on a
+ thread that a handler waits for, for example the event loop that a handler
+ runs coroutines on: use `await asyncio.to_thread(_flush)` there.
+ """
+ if _pid != os.getpid():
+ return
+ with _lock:
+ thread = _thread
+ if (
+ thread is None
+ or not thread.is_alive()
+ or _stop_marker is not None
+ or threading.current_thread() is thread
+ ):
+ return
+ marker = next(_marker_ids)
+ event = _markers[marker] = threading.Event()
+ _bindings._push_log_marker(marker)
+ event.wait()
+
+
+def _stop() -> None:
+ """At exit, hand the queued records to the logger and stop the thread, so
+ it does not call Python while the interpreter finalizes."""
+ global _logger, _stop_marker
+ if _pid != os.getpid():
+ return
+ with _lock:
+ thread = _thread
+ if thread is None or not thread.is_alive() or _stop_marker is not None:
+ return
+ _bindings._set_log_filters(None, logging.WARNING)
+ _stop_marker = marker = next(_marker_ids)
+ _markers[marker] = threading.Event()
+ _bindings._push_log_marker(marker)
+ thread.join()
+ _logger = None
+
+
+# Registered after `logging` registers `logging.shutdown`, so it runs first,
+# while the handlers are still open.
+atexit.register(_stop)
diff --git a/getstream-rtc/getstream_rtc/py.typed b/getstream-rtc/getstream_rtc/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/getstream-rtc/pyproject.toml b/getstream-rtc/pyproject.toml
new file mode 100644
index 00000000..b082ddb7
--- /dev/null
+++ b/getstream-rtc/pyproject.toml
@@ -0,0 +1,24 @@
+[project]
+name = "getstream-rtc"
+version = "0.1.0"
+description = "Python bindings for the Stream Video Rust SDK"
+requires-python = ">=3.10"
+dependencies = [
+ "numpy>=2.2.6",
+]
+
+[build-system]
+requires = ["maturin>=1.9.4,<2.0"]
+build-backend = "maturin"
+
+[tool.maturin]
+module-name = "getstream_rtc._bindings"
+exclude = ["tests/**"]
+
+[tool.uv]
+cache-keys = [
+ { file = "pyproject.toml" },
+ { file = "Cargo.toml" },
+ { file = "Cargo.lock" },
+ { file = "rustsrc/**/*.rs" },
+]
diff --git a/getstream-rtc/rust-toolchain.toml b/getstream-rtc/rust-toolchain.toml
new file mode 100644
index 00000000..724b93e6
--- /dev/null
+++ b/getstream-rtc/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "1.88"
+components = ["rustfmt", "clippy", "rust-analyzer"]
diff --git a/getstream-rtc/rustsrc/call.rs b/getstream-rtc/rustsrc/call.rs
new file mode 100644
index 00000000..fd1bc411
--- /dev/null
+++ b/getstream-rtc/rustsrc/call.rs
@@ -0,0 +1,327 @@
+use std::sync::Arc;
+
+use getstream::ClientConfig;
+use getstream::rtc::proto::models;
+use getstream::rtc::{JoinCallData, LocalTrack, RtcCall, RtcClient};
+use pyo3::prelude::*;
+use pyo3_async_runtimes::tokio::future_into_py;
+use tokio::sync::{Mutex, watch};
+
+use crate::call_end::{self, CallEnd};
+use crate::errors::{ConfigError, rtc_error, sdk_error};
+use crate::events::{EventStream, Source};
+use crate::participants::{CallStateSnapshot, RemoteParticipant, TrackType};
+use crate::subscriptions::SubscriptionConfig;
+use crate::tracks::{LocalAudioTrack, LocalVideoTrack, TrackQueue, TrackStream};
+
+enum Credentials {
+ /// Mints a user token for each join.
+ ApiSecret(getstream::Stream),
+ /// Joins with the user token of one user.
+ UserToken(RtcClient),
+}
+
+#[pyclass(frozen, module = "getstream_rtc")]
+pub struct Client {
+ inner: Credentials,
+}
+
+#[pymethods]
+impl Client {
+ #[new]
+ #[pyo3(signature = (api_key, api_secret=None, *, token=None, base_url=None, ws_url=None, log_bodies=false, call_event_capacity=None))]
+ fn new(
+ api_key: String,
+ api_secret: Option,
+ token: Option,
+ base_url: Option,
+ ws_url: Option,
+ log_bodies: bool,
+ call_event_capacity: Option,
+ ) -> PyResult {
+ let mut config = ClientConfig::default();
+ if let Some(base_url) = base_url {
+ config.base_url = base_url;
+ }
+ if let Some(ws_url) = ws_url {
+ config.coordinator_ws_url = ws_url;
+ }
+ if let Some(call_event_capacity) = call_event_capacity {
+ config.call_event_capacity = call_event_capacity;
+ }
+ config.log_bodies = log_bodies;
+ let inner = match (api_secret, token) {
+ (Some(api_secret), None) => Credentials::ApiSecret(
+ getstream::Stream::with_config(api_key, api_secret, config).map_err(sdk_error)?,
+ ),
+ (None, Some(token)) => Credentials::UserToken(
+ RtcClient::with_config(api_key, token, config).map_err(sdk_error)?,
+ ),
+ _ => {
+ return Err(ConfigError::new_err(
+ "pass exactly one of api_secret or token",
+ ));
+ }
+ };
+ Ok(Self { inner })
+ }
+
+ fn call(&self, call_type: String, call_id: String) -> Call {
+ let cid = format!("{call_type}:{call_id}");
+ let inner = match &self.inner {
+ Credentials::ApiSecret(stream) => stream.video().call(call_type, call_id).rtc(),
+ Credentials::UserToken(client) => client.call(call_type, call_id),
+ };
+ let end = call_end::watch_call_end(inner.client_events());
+ let tracks = TrackQueue::register(&inner);
+ Call {
+ inner,
+ cid,
+ tracks,
+ end,
+ join_lock: Arc::default(),
+ }
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc")]
+pub struct Call {
+ inner: RtcCall,
+ /// `":"`, for the `call_cid` of the SFU events.
+ cid: String,
+ tracks: Arc,
+ end: watch::Receiver,
+ /// Python sees a cancelled `join()` at once, but the SDK future is dropped
+ /// (and resets the call to `Idle`) only later on the tokio runtime. The
+ /// next `join()` waits for this lock until that has happened.
+ join_lock: Arc>,
+}
+
+#[pymethods]
+impl Call {
+ #[pyo3(signature = (user_id, create=true))]
+ fn join<'py>(
+ &self,
+ py: Python<'py>,
+ user_id: String,
+ create: bool,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let join_lock = self.join_lock.clone();
+ let data = if create {
+ JoinCallData::create(user_id)
+ } else {
+ JoinCallData::new(user_id)
+ };
+ future_into_py(py, async move {
+ let _join_guard = join_lock.lock_owned().await;
+ // Pinned in a local declared after the guard, so a cancelled join
+ // drops the SDK future before it releases the lock.
+ let join = call.join(data);
+ tokio::pin!(join);
+ join.as_mut().await.map_err(rtc_error)
+ })
+ }
+
+ fn leave<'py>(&self, py: Python<'py>) -> PyResult> {
+ let call = self.inner.clone();
+ future_into_py(py, async move { call.leave().await.map_err(rtc_error) })
+ }
+
+ fn session_id<'py>(&self, py: Python<'py>) -> PyResult> {
+ let call = self.inner.clone();
+ future_into_py(py, async move { Ok(call.session_id().await) })
+ }
+
+ #[getter]
+ fn calling_state(&self, py: Python<'_>) -> CallingState {
+ py.detach(|| self.inner.calling_state()).into()
+ }
+
+ fn participants(&self, py: Python<'_>) -> Vec {
+ py.detach(|| {
+ self.inner
+ .participants()
+ .into_iter()
+ .map(Into::into)
+ .collect()
+ })
+ }
+
+ fn call_state(&self, py: Python<'_>) -> CallStateSnapshot {
+ py.detach(|| self.inner.call_state()).into()
+ }
+
+ fn sfu_events(&self, py: Python<'_>) -> EventStream {
+ py.detach(|| {
+ let source = Source::Sfu(self.inner.sfu_events());
+ EventStream::new(source, &self.inner, self.cid.clone(), self.end.clone())
+ })
+ }
+
+ fn coordinator_events(&self, py: Python<'_>) -> EventStream {
+ py.detach(|| {
+ let source = Source::Coordinator(self.inner.coordinator_events());
+ EventStream::new(source, &self.inner, self.cid.clone(), self.end.clone())
+ })
+ }
+
+ fn client_events(&self, py: Python<'_>) -> EventStream {
+ py.detach(|| {
+ let source = Source::Client(self.inner.client_events());
+ EventStream::new(source, &self.inner, self.cid.clone(), self.end.clone())
+ })
+ }
+
+ fn tracks(&self, py: Python<'_>) -> TrackStream {
+ py.detach(|| TrackStream::new(self.tracks.clone(), &self.inner, self.end.clone()))
+ }
+
+ fn publish_audio<'py>(
+ &self,
+ py: Python<'py>,
+ track: &Bound<'py, LocalAudioTrack>,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let track = track.get().inner.clone();
+ future_into_py(py, async move {
+ call.publish_audio(track).await.map_err(rtc_error)
+ })
+ }
+
+ fn publish_video<'py>(
+ &self,
+ py: Python<'py>,
+ track: &Bound<'py, LocalVideoTrack>,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let track = track.get().inner.clone();
+ future_into_py(py, async move {
+ call.publish_video(track).await.map_err(rtc_error)
+ })
+ }
+
+ fn publish_screen_share<'py>(
+ &self,
+ py: Python<'py>,
+ track: &Bound<'py, LocalVideoTrack>,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let track = track.get().inner.clone();
+ future_into_py(py, async move {
+ call.publish_screen_share(track).await.map_err(rtc_error)
+ })
+ }
+
+ fn stop_publish_audio<'py>(
+ &self,
+ py: Python<'py>,
+ track: &Bound<'py, LocalAudioTrack>,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let track = LocalTrack::Audio(track.get().inner.clone());
+ future_into_py(py, async move {
+ call.stop_publish(track).await.map_err(rtc_error)
+ })
+ }
+
+ fn stop_publish_video<'py>(
+ &self,
+ py: Python<'py>,
+ track: &Bound<'py, LocalVideoTrack>,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let track = LocalTrack::Video {
+ track: track.get().inner.clone(),
+ track_type: models::TrackType::Video,
+ };
+ future_into_py(py, async move {
+ call.stop_publish(track).await.map_err(rtc_error)
+ })
+ }
+
+ fn stop_publish_screen_share<'py>(
+ &self,
+ py: Python<'py>,
+ track: &Bound<'py, LocalVideoTrack>,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let track = LocalTrack::Video {
+ track: track.get().inner.clone(),
+ track_type: models::TrackType::ScreenShare,
+ };
+ future_into_py(py, async move {
+ call.stop_publish(track).await.map_err(rtc_error)
+ })
+ }
+
+ fn mute_track<'py>(
+ &self,
+ py: Python<'py>,
+ track_type: TrackType,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ future_into_py(py, async move {
+ call.mute_track(track_type.into()).await.map_err(rtc_error)
+ })
+ }
+
+ fn unmute_track<'py>(
+ &self,
+ py: Python<'py>,
+ track_type: TrackType,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ future_into_py(py, async move {
+ call.unmute_track(track_type.into())
+ .await
+ .map_err(rtc_error)
+ })
+ }
+
+ fn update_subscriptions<'py>(
+ &self,
+ py: Python<'py>,
+ config: SubscriptionConfig,
+ ) -> PyResult> {
+ let call = self.inner.clone();
+ let config = getstream::rtc::SubscriptionConfig::from(config);
+ future_into_py(py, async move {
+ call.update_subscriptions(config).await.map_err(rtc_error)
+ })
+ }
+}
+
+#[pyclass(
+ frozen,
+ module = "getstream_rtc",
+ eq,
+ eq_int,
+ skip_from_py_object,
+ rename_all = "SCREAMING_SNAKE_CASE"
+)]
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum CallingState {
+ Idle,
+ Joining,
+ Joined,
+ Reconnecting,
+ Migrating,
+ ReconnectingFailed,
+ Left,
+}
+
+impl From for CallingState {
+ fn from(state: getstream::rtc::CallingState) -> Self {
+ use getstream::rtc::CallingState as Sdk;
+ match state {
+ Sdk::Idle => Self::Idle,
+ Sdk::Joining => Self::Joining,
+ Sdk::Joined => Self::Joined,
+ Sdk::Reconnecting => Self::Reconnecting,
+ Sdk::Migrating => Self::Migrating,
+ Sdk::ReconnectingFailed => Self::ReconnectingFailed,
+ Sdk::Left => Self::Left,
+ }
+ }
+}
diff --git a/getstream-rtc/rustsrc/call_end.rs b/getstream-rtc/rustsrc/call_end.rs
new file mode 100644
index 00000000..e96965f0
--- /dev/null
+++ b/getstream-rtc/rustsrc/call_end.rs
@@ -0,0 +1,73 @@
+use getstream::rtc::{CallingState, ClientCallEvent, RtcCall};
+use tokio::sync::broadcast::Receiver;
+use tokio::sync::broadcast::error::RecvError;
+use tokio::sync::watch;
+
+/// The call ends when this participant's state becomes `Left`; the SDK leaves
+/// on the SFU `call_ended` and on the coordinator `call.ended`. `ends` counts
+/// the ends.
+#[derive(Clone, Copy)]
+pub struct CallEnd {
+ ends: u64,
+}
+
+/// Watches the call's client events on the pyo3-async-runtimes tokio runtime. The
+/// task stops when every receiver is dropped or the SDK core is gone.
+pub fn watch_call_end(mut events: Receiver) -> watch::Receiver {
+ let (sender, receiver) = watch::channel(CallEnd { ends: 0 });
+ pyo3_async_runtimes::tokio::get_runtime().spawn(async move {
+ loop {
+ let event = tokio::select! {
+ event = events.recv() => event,
+ () = sender.closed() => return,
+ };
+ match event {
+ Ok(ClientCallEvent::CallingStateChanged(CallingState::Left)) => {
+ sender.send_modify(|end| end.ends = end.ends.wrapping_add(1));
+ }
+ Ok(_) | Err(RecvError::Lagged(_)) => {}
+ Err(RecvError::Closed) => return,
+ }
+ }
+ });
+ receiver
+}
+
+/// The end count that a new stream waits for, or `None` if the call has
+/// already ended. The SDK sets `Left` before `leave()` returns, but the watcher
+/// counts that end later, so the state comes from the SDK. Takes the SDK
+/// lifecycle lock: call it without the GIL.
+pub fn start(call: &RtcCall, end: &watch::Receiver) -> Option {
+ // Read before the state, so an end between the two reads gives `None`
+ // and not a count that the stream would wait past.
+ let ends = end.borrow().ends;
+ (call.calling_state() != CallingState::Left).then_some(ends)
+}
+
+/// Resolves when the call has ended after `start` was taken.
+pub async fn ended(mut end: watch::Receiver, start: Option) {
+ if let Some(start) = start {
+ // An error means the watcher stopped, which happens only when the SDK
+ // core is gone.
+ let _ = end.wait_for(|end| end.ends != start).await;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use getstream::rtc::RtcClient;
+
+ use super::*;
+
+ #[tokio::test]
+ async fn a_stream_created_after_leave_has_ended() {
+ let call = RtcClient::new("key", "token")
+ .expect("a client")
+ .call("default", "call");
+ call.leave().await.expect("a leave");
+ // No watcher has seen the leave yet.
+ let (_sender, end) = watch::channel(CallEnd { ends: 0 });
+
+ assert_eq!(start(&call, &end), None);
+ }
+}
diff --git a/getstream-rtc/rustsrc/errors.rs b/getstream-rtc/rustsrc/errors.rs
new file mode 100644
index 00000000..1c052025
--- /dev/null
+++ b/getstream-rtc/rustsrc/errors.rs
@@ -0,0 +1,49 @@
+use pyo3::PyErr;
+use pyo3::import_exception;
+
+import_exception!(getstream_rtc.errors, RustError);
+import_exception!(getstream_rtc.errors, ConfigError);
+import_exception!(getstream_rtc.errors, RtcError);
+import_exception!(getstream_rtc.errors, ApiError);
+import_exception!(getstream_rtc.errors, CoordinatorError);
+import_exception!(getstream_rtc.errors, IllegalStateError);
+import_exception!(getstream_rtc.errors, MediaError);
+import_exception!(getstream_rtc.errors, PermissionDeniedError);
+import_exception!(getstream_rtc.errors, PcmQueueOverflowError);
+import_exception!(getstream_rtc.errors, PcmRateMismatchError);
+
+pub fn sdk_error(err: getstream::Error) -> PyErr {
+ let message = err.to_string();
+ match err {
+ getstream::Error::Config(_) => ConfigError::new_err(message),
+ _ => RustError::new_err(message),
+ }
+}
+
+pub fn rtc_error(err: getstream::rtc::RtcError) -> PyErr {
+ use getstream::rtc::RtcError as Sdk;
+ let message = err.to_string();
+ match err {
+ Sdk::Api(api) => ApiError::new_err((
+ message,
+ api.code,
+ api.status,
+ api.message,
+ api.unrecoverable,
+ )),
+ Sdk::Coordinator(_) => CoordinatorError::new_err(message),
+ Sdk::IllegalState(_) => IllegalStateError::new_err(message),
+ Sdk::Media(_) => MediaError::new_err(message),
+ Sdk::PermissionDenied { capability } => {
+ PermissionDeniedError::new_err((message, capability))
+ }
+ Sdk::PcmQueueOverflow {
+ dropped_samples,
+ capacity_samples,
+ } => PcmQueueOverflowError::new_err((message, dropped_samples, capacity_samples)),
+ Sdk::PcmRateMismatch { expected, actual } => {
+ PcmRateMismatchError::new_err((message, expected, actual))
+ }
+ _ => RtcError::new_err(message),
+ }
+}
diff --git a/getstream-rtc/rustsrc/events.rs b/getstream-rtc/rustsrc/events.rs
new file mode 100644
index 00000000..3ffff6f2
--- /dev/null
+++ b/getstream-rtc/rustsrc/events.rs
@@ -0,0 +1,1520 @@
+use std::sync::Arc;
+
+use getstream::rtc::proto::event;
+use getstream::rtc::proto::models::{self, CallEndedReason};
+use getstream::rtc::{ClientCallEvent, RtcCall, SfuCallEvent};
+use pyo3::IntoPyObjectExt;
+use pyo3::exceptions::PyStopAsyncIteration;
+use pyo3::prelude::*;
+use pyo3::types::{PyDict, PyList};
+use pyo3_async_runtimes::tokio::future_into_py;
+use tokio::sync::broadcast::Receiver;
+use tokio::sync::broadcast::error::RecvError;
+use tokio::sync::{Mutex, watch};
+
+use crate::call::CallingState;
+use crate::call_end::{self, CallEnd};
+use crate::participants::{RemoteParticipant, TrackType};
+use crate::repr::repr;
+
+/// The base class of all events. `name` is the stable SDK event name: the
+/// `SfuEvent` oneof field name for SFU events, the `type` for coordinator events.
+#[pyclass(frozen, subclass, name = "CallEvent", module = "getstream_rtc")]
+pub struct CallEventBase {
+ #[pyo3(get)]
+ name: String,
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct ParticipantJoined {
+ /// The cid of this call, `":"`.
+ #[pyo3(get)]
+ call_cid: String,
+ #[pyo3(get)]
+ participant: RemoteParticipant,
+}
+
+#[pymethods]
+impl ParticipantJoined {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ParticipantJoined(call_cid={}, participant={})",
+ repr(py, &self.call_cid)?,
+ repr(py, self.participant.clone())?
+ ))
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct ParticipantLeft {
+ /// The cid of this call, `":"`.
+ #[pyo3(get)]
+ call_cid: String,
+ #[pyo3(get)]
+ participant: RemoteParticipant,
+}
+
+#[pymethods]
+impl ParticipantLeft {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ParticipantLeft(call_cid={}, participant={})",
+ repr(py, &self.call_cid)?,
+ repr(py, self.participant.clone())?
+ ))
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct TrackPublished {
+ #[pyo3(get)]
+ user_id: String,
+ #[pyo3(get)]
+ session_id: String,
+ #[pyo3(get)]
+ track_type: TrackType,
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ participant: Option,
+}
+
+#[pymethods]
+impl TrackPublished {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "TrackPublished(user_id={}, session_id={}, track_type={})",
+ repr(py, &self.user_id)?,
+ repr(py, &self.session_id)?,
+ repr(py, self.track_type)?,
+ ))
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct TrackUnpublished {
+ #[pyo3(get)]
+ user_id: String,
+ #[pyo3(get)]
+ session_id: String,
+ #[pyo3(get)]
+ track_type: TrackType,
+ /// The protobuf `TrackUnpublishReason` value.
+ #[pyo3(get)]
+ cause: i32,
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ participant: Option,
+}
+
+#[pymethods]
+impl TrackUnpublished {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "TrackUnpublished(user_id={}, session_id={}, track_type={}, cause={})",
+ repr(py, &self.user_id)?,
+ repr(py, &self.session_id)?,
+ repr(py, self.track_type)?,
+ self.cause,
+ ))
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct CallEnded {
+ /// The protobuf `CallEndedReason` value.
+ #[pyo3(get)]
+ reason: i32,
+}
+
+#[pymethods]
+impl CallEnded {
+ /// For an end that comes without the SFU `call_ended`, for example when the
+ /// coordinator ends the call.
+ #[new]
+ fn new() -> PyClassInitializer {
+ let name = SfuCallEvent::CallEnded {
+ reason: CallEndedReason::Unspecified,
+ }
+ .name();
+ PyClassInitializer::from(CallEventBase {
+ name: name.to_owned(),
+ })
+ .add_subclass(Self {
+ reason: CallEndedReason::Unspecified as i32,
+ })
+ }
+
+ fn __repr__(&self) -> String {
+ format!("CallEnded(reason={})", self.reason)
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct CallingStateChanged {
+ #[pyo3(get)]
+ state: CallingState,
+}
+
+#[pymethods]
+impl CallingStateChanged {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "CallingStateChanged(state={})",
+ repr(py, self.state)?
+ ))
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct ParticipantUpdated {
+ /// The cid of this call, `":"`.
+ #[pyo3(get)]
+ call_cid: String,
+ #[pyo3(get)]
+ participant: RemoteParticipant,
+}
+
+#[pymethods]
+impl ParticipantUpdated {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ParticipantUpdated(call_cid={}, participant={})",
+ repr(py, &self.call_cid)?,
+ repr(py, self.participant.clone())?
+ ))
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct DominantSpeakerChanged {
+ #[pyo3(get)]
+ user_id: String,
+ #[pyo3(get)]
+ session_id: String,
+}
+
+#[pymethods]
+impl DominantSpeakerChanged {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "DominantSpeakerChanged(user_id={}, session_id={})",
+ repr(py, &self.user_id)?,
+ repr(py, &self.session_id)?
+ ))
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct AudioLevel {
+ #[pyo3(get)]
+ user_id: String,
+ #[pyo3(get)]
+ session_id: String,
+ /// 0.0 is silence, 1.0 the loudest.
+ #[pyo3(get)]
+ level: f32,
+ #[pyo3(get)]
+ is_speaking: bool,
+}
+
+#[pymethods]
+impl AudioLevel {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "AudioLevel(user_id={}, session_id={}, level={}, is_speaking={})",
+ repr(py, &self.user_id)?,
+ repr(py, &self.session_id)?,
+ self.level,
+ repr(py, self.is_speaking)?
+ ))
+ }
+}
+
+impl From for AudioLevel {
+ fn from(level: event::AudioLevel) -> Self {
+ Self {
+ user_id: level.user_id,
+ session_id: level.session_id,
+ level: level.level,
+ is_speaking: level.is_speaking,
+ }
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct AudioLevelChanged {
+ #[pyo3(get)]
+ audio_levels: Vec,
+}
+
+#[pymethods]
+impl AudioLevelChanged {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "AudioLevelChanged(audio_levels={})",
+ repr(py, self.audio_levels.clone())?
+ ))
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct ConnectionQualityInfo {
+ #[pyo3(get)]
+ user_id: String,
+ #[pyo3(get)]
+ session_id: String,
+ /// The protobuf `ConnectionQuality` value.
+ #[pyo3(get)]
+ connection_quality: i32,
+}
+
+#[pymethods]
+impl ConnectionQualityInfo {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ConnectionQualityInfo(user_id={}, session_id={}, connection_quality={})",
+ repr(py, &self.user_id)?,
+ repr(py, &self.session_id)?,
+ self.connection_quality
+ ))
+ }
+}
+
+impl From for ConnectionQualityInfo {
+ fn from(info: event::ConnectionQualityInfo) -> Self {
+ Self {
+ user_id: info.user_id,
+ session_id: info.session_id,
+ connection_quality: info.connection_quality,
+ }
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct ConnectionQualityChanged {
+ #[pyo3(get)]
+ connection_quality_updates: Vec,
+}
+
+#[pymethods]
+impl ConnectionQualityChanged {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ConnectionQualityChanged(connection_quality_updates={})",
+ repr(py, self.connection_quality_updates.clone())?
+ ))
+ }
+}
+
+/// Sent by the SDK when the SFU's participant count changes.
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct ParticipantCountChanged {
+ #[pyo3(get)]
+ total: u32,
+ #[pyo3(get)]
+ anonymous: u32,
+}
+
+#[pymethods]
+impl ParticipantCountChanged {
+ fn __repr__(&self) -> String {
+ format!(
+ "ParticipantCountChanged(total={}, anonymous={})",
+ self.total, self.anonymous
+ )
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct Pin {
+ #[pyo3(get)]
+ user_id: String,
+ #[pyo3(get)]
+ session_id: String,
+}
+
+#[pymethods]
+impl Pin {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "Pin(user_id={}, session_id={})",
+ repr(py, &self.user_id)?,
+ repr(py, &self.session_id)?
+ ))
+ }
+}
+
+impl From for Pin {
+ fn from(pin: models::Pin) -> Self {
+ Self {
+ user_id: pin.user_id,
+ session_id: pin.session_id,
+ }
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct PinsChanged {
+ #[pyo3(get)]
+ pins: Vec,
+}
+
+#[pymethods]
+impl PinsChanged {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "PinsChanged(pins={})",
+ repr(py, self.pins.clone())?
+ ))
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct InboundVideoState {
+ #[pyo3(get)]
+ user_id: String,
+ #[pyo3(get)]
+ session_id: String,
+ #[pyo3(get)]
+ track_type: TrackType,
+ #[pyo3(get)]
+ paused: bool,
+}
+
+#[pymethods]
+impl InboundVideoState {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "InboundVideoState(user_id={}, session_id={}, track_type={}, paused={})",
+ repr(py, &self.user_id)?,
+ repr(py, &self.session_id)?,
+ repr(py, self.track_type)?,
+ repr(py, self.paused)?
+ ))
+ }
+}
+
+impl From for InboundVideoState {
+ fn from(state: event::InboundVideoState) -> Self {
+ Self {
+ user_id: state.user_id,
+ session_id: state.session_id,
+ track_type: TrackType::from_i32(state.track_type),
+ paused: state.paused,
+ }
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct InboundStateNotification {
+ #[pyo3(get)]
+ inbound_video_states: Vec,
+}
+
+#[pymethods]
+impl InboundStateNotification {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "InboundStateNotification(inbound_video_states={})",
+ repr(py, self.inbound_video_states.clone())?
+ ))
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct CallGrants {
+ #[pyo3(get)]
+ can_publish_audio: bool,
+ #[pyo3(get)]
+ can_publish_video: bool,
+ #[pyo3(get)]
+ can_screenshare: bool,
+}
+
+#[pymethods]
+impl CallGrants {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "CallGrants(can_publish_audio={}, can_publish_video={}, can_screenshare={})",
+ repr(py, self.can_publish_audio)?,
+ repr(py, self.can_publish_video)?,
+ repr(py, self.can_screenshare)?
+ ))
+ }
+}
+
+impl From for CallGrants {
+ fn from(grants: models::CallGrants) -> Self {
+ Self {
+ can_publish_audio: grants.can_publish_audio,
+ can_publish_video: grants.can_publish_video,
+ can_screenshare: grants.can_screenshare,
+ }
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct CallGrantsUpdated {
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ current_grants: Option,
+ #[pyo3(get)]
+ message: String,
+}
+
+#[pymethods]
+impl CallGrantsUpdated {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "CallGrantsUpdated(current_grants={}, message={})",
+ repr(py, self.current_grants.clone())?,
+ repr(py, &self.message)?
+ ))
+ }
+}
+
+/// Sent after the SDK has handled the SFU's ICE restart request.
+#[pyclass(
+ frozen,
+ extends = CallEventBase,
+ name = "ICERestart",
+ module = "getstream_rtc"
+)]
+pub struct IceRestart {
+ /// The protobuf `PeerType` value.
+ #[pyo3(get)]
+ peer_type: i32,
+}
+
+#[pymethods]
+impl IceRestart {
+ fn __repr__(&self) -> String {
+ format!("ICERestart(peer_type={})", self.peer_type)
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct Codec {
+ #[pyo3(get)]
+ payload_type: u32,
+ #[pyo3(get)]
+ name: String,
+ #[pyo3(get)]
+ clock_rate: u32,
+ #[pyo3(get)]
+ encoding_parameters: String,
+ #[pyo3(get)]
+ fmtp: String,
+}
+
+#[pymethods]
+impl Codec {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "Codec(name={}, payload_type={}, clock_rate={})",
+ repr(py, &self.name)?,
+ self.payload_type,
+ self.clock_rate
+ ))
+ }
+}
+
+impl From for Codec {
+ fn from(codec: models::Codec) -> Self {
+ Self {
+ payload_type: codec.payload_type,
+ name: codec.name,
+ clock_rate: codec.clock_rate,
+ encoding_parameters: codec.encoding_parameters,
+ fmtp: codec.fmtp,
+ }
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct VideoDimension {
+ #[pyo3(get)]
+ width: u32,
+ #[pyo3(get)]
+ height: u32,
+}
+
+#[pymethods]
+impl VideoDimension {
+ fn __repr__(&self) -> String {
+ format!(
+ "VideoDimension(width={}, height={})",
+ self.width, self.height
+ )
+ }
+}
+
+impl From for VideoDimension {
+ fn from(dimension: models::VideoDimension) -> Self {
+ Self {
+ width: dimension.width,
+ height: dimension.height,
+ }
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct AudioBitrate {
+ /// The protobuf `AudioBitrateProfile` value.
+ #[pyo3(get)]
+ profile: i32,
+ #[pyo3(get)]
+ bitrate: i32,
+}
+
+#[pymethods]
+impl AudioBitrate {
+ fn __repr__(&self) -> String {
+ format!(
+ "AudioBitrate(profile={}, bitrate={})",
+ self.profile, self.bitrate
+ )
+ }
+}
+
+impl From for AudioBitrate {
+ fn from(bitrate: models::AudioBitrate) -> Self {
+ Self {
+ profile: bitrate.profile,
+ bitrate: bitrate.bitrate,
+ }
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct PublishOption {
+ #[pyo3(get)]
+ track_type: TrackType,
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ codec: Option,
+ #[pyo3(get)]
+ bitrate: i32,
+ #[pyo3(get)]
+ fps: i32,
+ #[pyo3(get)]
+ max_spatial_layers: i32,
+ #[pyo3(get)]
+ max_temporal_layers: i32,
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ video_dimension: Option,
+ #[pyo3(get)]
+ id: i32,
+ #[pyo3(get)]
+ use_single_layer: bool,
+ #[pyo3(get)]
+ audio_bitrate_profiles: Vec,
+ /// The protobuf `DegradationPreference` value.
+ #[pyo3(get)]
+ degradation_preference: i32,
+}
+
+#[pymethods]
+impl PublishOption {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "PublishOption(id={}, track_type={}, codec={}, bitrate={})",
+ self.id,
+ repr(py, self.track_type)?,
+ repr(py, self.codec.clone())?,
+ self.bitrate
+ ))
+ }
+}
+
+impl From for PublishOption {
+ fn from(option: models::PublishOption) -> Self {
+ Self {
+ track_type: TrackType::from_i32(option.track_type),
+ codec: option.codec.map(Into::into),
+ bitrate: option.bitrate,
+ fps: option.fps,
+ max_spatial_layers: option.max_spatial_layers,
+ max_temporal_layers: option.max_temporal_layers,
+ video_dimension: option.video_dimension.map(Into::into),
+ id: option.id,
+ use_single_layer: option.use_single_layer,
+ audio_bitrate_profiles: option
+ .audio_bitrate_profiles
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ degradation_preference: option.degradation_preference,
+ }
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct ChangePublishOptions {
+ #[pyo3(get)]
+ publish_options: Vec,
+ #[pyo3(get)]
+ reason: String,
+}
+
+#[pymethods]
+impl ChangePublishOptions {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ChangePublishOptions(publish_options={}, reason={})",
+ repr(py, self.publish_options.clone())?,
+ repr(py, &self.reason)?
+ ))
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct AudioSender {
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ codec: Option,
+ #[pyo3(get)]
+ track_type: TrackType,
+ #[pyo3(get)]
+ publish_option_id: i32,
+}
+
+#[pymethods]
+impl AudioSender {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "AudioSender(codec={}, track_type={}, publish_option_id={})",
+ repr(py, self.codec.clone())?,
+ repr(py, self.track_type)?,
+ self.publish_option_id
+ ))
+ }
+}
+
+impl From for AudioSender {
+ fn from(sender: event::AudioSender) -> Self {
+ Self {
+ codec: sender.codec.map(Into::into),
+ track_type: TrackType::from_i32(sender.track_type),
+ publish_option_id: sender.publish_option_id,
+ }
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct VideoLayerSetting {
+ #[pyo3(get)]
+ name: String,
+ #[pyo3(get)]
+ active: bool,
+ #[pyo3(get)]
+ max_bitrate: i32,
+ #[pyo3(get)]
+ scale_resolution_down_by: f32,
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ codec: Option,
+ #[pyo3(get)]
+ max_framerate: u32,
+ #[pyo3(get)]
+ scalability_mode: String,
+}
+
+#[pymethods]
+impl VideoLayerSetting {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "VideoLayerSetting(name={}, active={}, max_bitrate={})",
+ repr(py, &self.name)?,
+ repr(py, self.active)?,
+ self.max_bitrate
+ ))
+ }
+}
+
+impl From for VideoLayerSetting {
+ fn from(layer: event::VideoLayerSetting) -> Self {
+ Self {
+ name: layer.name,
+ active: layer.active,
+ max_bitrate: layer.max_bitrate,
+ scale_resolution_down_by: layer.scale_resolution_down_by,
+ codec: layer.codec.map(Into::into),
+ max_framerate: layer.max_framerate,
+ scalability_mode: layer.scalability_mode,
+ }
+ }
+}
+
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct VideoSender {
+ /// `None` if the SFU did not send it.
+ #[pyo3(get)]
+ codec: Option,
+ #[pyo3(get)]
+ layers: Vec,
+ #[pyo3(get)]
+ track_type: TrackType,
+ #[pyo3(get)]
+ publish_option_id: i32,
+ /// The protobuf `DegradationPreference` value.
+ #[pyo3(get)]
+ degradation_preference: i32,
+}
+
+#[pymethods]
+impl VideoSender {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "VideoSender(codec={}, track_type={}, layers={})",
+ repr(py, self.codec.clone())?,
+ repr(py, self.track_type)?,
+ repr(py, self.layers.clone())?
+ ))
+ }
+}
+
+impl From for VideoSender {
+ fn from(sender: event::VideoSender) -> Self {
+ Self {
+ codec: sender.codec.map(Into::into),
+ layers: sender.layers.into_iter().map(Into::into).collect(),
+ track_type: TrackType::from_i32(sender.track_type),
+ publish_option_id: sender.publish_option_id,
+ degradation_preference: sender.degradation_preference,
+ }
+ }
+}
+
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct ChangePublishQuality {
+ #[pyo3(get)]
+ audio_senders: Vec,
+ #[pyo3(get)]
+ video_senders: Vec,
+}
+
+#[pymethods]
+impl ChangePublishQuality {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ChangePublishQuality(audio_senders={}, video_senders={})",
+ repr(py, self.audio_senders.clone())?,
+ repr(py, self.video_senders.clone())?
+ ))
+ }
+}
+
+/// The protobuf `models.Error` inside the `Error` event.
+#[pyclass(frozen, module = "getstream_rtc", skip_from_py_object)]
+#[derive(Clone)]
+pub struct ErrorDetails {
+ /// The protobuf `ErrorCode` value.
+ #[pyo3(get)]
+ code: i32,
+ #[pyo3(get)]
+ message: String,
+ #[pyo3(get)]
+ should_retry: bool,
+}
+
+#[pymethods]
+impl ErrorDetails {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "ErrorDetails(code={}, message={}, should_retry={})",
+ self.code,
+ repr(py, &self.message)?,
+ repr(py, self.should_retry)?
+ ))
+ }
+}
+
+#[pyclass(
+ frozen,
+ extends = CallEventBase,
+ name = "Error",
+ module = "getstream_rtc"
+)]
+pub struct SfuError {
+ #[pyo3(get)]
+ error: ErrorDetails,
+ /// The protobuf `WebsocketReconnectStrategy` value.
+ #[pyo3(get)]
+ reconnect_strategy: i32,
+}
+
+#[pymethods]
+impl SfuError {
+ fn __repr__(&self, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "Error(error={}, reconnect_strategy={})",
+ repr(py, self.error.clone())?,
+ self.reconnect_strategy
+ ))
+ }
+}
+
+/// Sent by the wrapper when this stream fell behind and lost `skipped` events.
+/// The stream continues with the oldest event that is still buffered.
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct EventsLagged {
+ #[pyo3(get)]
+ skipped: u64,
+}
+
+#[pymethods]
+impl EventsLagged {
+ fn __repr__(&self) -> String {
+ format!("EventsLagged(skipped={})", self.skipped)
+ }
+}
+
+/// A coordinator event of this call. `name` is its JSON `type`.
+#[pyclass(frozen, extends = CallEventBase, module = "getstream_rtc")]
+pub struct CoordinatorEvent {
+ /// The whole JSON message.
+ #[pyo3(get)]
+ data: Py,
+}
+
+#[pymethods]
+impl CoordinatorEvent {
+ fn __repr__(slf: PyRef<'_, Self>, py: Python<'_>) -> PyResult {
+ Ok(format!(
+ "CoordinatorEvent(name={})",
+ repr(py, &slf.as_super().name)?
+ ))
+ }
+}
+
+enum Event {
+ ParticipantJoined(ParticipantJoined),
+ ParticipantLeft(ParticipantLeft),
+ TrackPublished(TrackPublished),
+ TrackUnpublished(TrackUnpublished),
+ CallEnded(CallEnded),
+ ParticipantUpdated(ParticipantUpdated),
+ DominantSpeakerChanged(DominantSpeakerChanged),
+ AudioLevelChanged(AudioLevelChanged),
+ ConnectionQualityChanged(ConnectionQualityChanged),
+ ParticipantCountChanged(ParticipantCountChanged),
+ PinsChanged(PinsChanged),
+ InboundStateNotification(InboundStateNotification),
+ CallGrantsUpdated(CallGrantsUpdated),
+ IceRestart(IceRestart),
+ ChangePublishOptions(ChangePublishOptions),
+ ChangePublishQuality(ChangePublishQuality),
+ SfuError(SfuError),
+ EventsLagged(EventsLagged),
+ CallingStateChanged(CallingStateChanged),
+ /// The JSON message; it becomes a `dict` when the Python object is made,
+ /// with the GIL.
+ Coordinator(serde_json::Value),
+}
+
+impl Event {
+ /// Returns `None` for SDK events that have no Python class.
+ fn from_sfu(event: SfuCallEvent, call_cid: &str) -> Option {
+ Some(match event {
+ SfuCallEvent::ParticipantJoined(participant) => {
+ Self::ParticipantJoined(ParticipantJoined {
+ call_cid: call_cid.to_owned(),
+ participant: participant.into(),
+ })
+ }
+ SfuCallEvent::ParticipantLeft(participant) => Self::ParticipantLeft(ParticipantLeft {
+ call_cid: call_cid.to_owned(),
+ participant: participant.into(),
+ }),
+ SfuCallEvent::TrackPublished {
+ user_id,
+ session_id,
+ track_type,
+ participant,
+ } => Self::TrackPublished(TrackPublished {
+ user_id,
+ session_id,
+ track_type: track_type.into(),
+ participant: participant.map(Into::into),
+ }),
+ SfuCallEvent::TrackUnpublished {
+ user_id,
+ session_id,
+ track_type,
+ cause,
+ participant,
+ } => Self::TrackUnpublished(TrackUnpublished {
+ user_id,
+ session_id,
+ track_type: track_type.into(),
+ cause: cause as i32,
+ participant: participant.map(Into::into),
+ }),
+ SfuCallEvent::CallEnded { reason } => Self::CallEnded(CallEnded {
+ reason: reason as i32,
+ }),
+ SfuCallEvent::ParticipantUpdated(participant) => {
+ Self::ParticipantUpdated(ParticipantUpdated {
+ call_cid: call_cid.to_owned(),
+ participant: participant.into(),
+ })
+ }
+ SfuCallEvent::DominantSpeakerChanged {
+ user_id,
+ session_id,
+ } => Self::DominantSpeakerChanged(DominantSpeakerChanged {
+ user_id,
+ session_id,
+ }),
+ SfuCallEvent::AudioLevelChanged(levels) => Self::AudioLevelChanged(AudioLevelChanged {
+ audio_levels: levels.into_iter().map(Into::into).collect(),
+ }),
+ SfuCallEvent::ConnectionQualityChanged(updates) => {
+ Self::ConnectionQualityChanged(ConnectionQualityChanged {
+ connection_quality_updates: updates.into_iter().map(Into::into).collect(),
+ })
+ }
+ SfuCallEvent::ParticipantCountChanged(count) => {
+ Self::ParticipantCountChanged(ParticipantCountChanged {
+ total: count.total,
+ anonymous: count.anonymous,
+ })
+ }
+ SfuCallEvent::PinsUpdated(pins) => Self::PinsChanged(PinsChanged {
+ pins: pins.into_iter().map(Into::into).collect(),
+ }),
+ SfuCallEvent::InboundStateChanged(states) => {
+ Self::InboundStateNotification(InboundStateNotification {
+ inbound_video_states: states.into_iter().map(Into::into).collect(),
+ })
+ }
+ SfuCallEvent::CallGrantsUpdated(event) => Self::CallGrantsUpdated(CallGrantsUpdated {
+ current_grants: event.current_grants.map(Into::into),
+ message: event.message,
+ }),
+ SfuCallEvent::IceRestarted(peer_type) => Self::IceRestart(IceRestart {
+ peer_type: peer_type as i32,
+ }),
+ SfuCallEvent::PublishOptionsChanged {
+ publish_options,
+ reason,
+ } => Self::ChangePublishOptions(ChangePublishOptions {
+ publish_options: publish_options.into_iter().map(Into::into).collect(),
+ reason,
+ }),
+ SfuCallEvent::PublishQualityChanged(quality) => {
+ Self::ChangePublishQuality(ChangePublishQuality {
+ audio_senders: quality.audio_senders.into_iter().map(Into::into).collect(),
+ video_senders: quality.video_senders.into_iter().map(Into::into).collect(),
+ })
+ }
+ SfuCallEvent::Error(error) => Self::SfuError(SfuError {
+ error: ErrorDetails {
+ code: error.code,
+ message: error.message,
+ should_retry: error.should_retry,
+ },
+ reconnect_strategy: error.reconnect_strategy,
+ }),
+ other => {
+ tracing::debug!(event = other.name(), "stream.rtc.events.unsupported");
+ return None;
+ }
+ })
+ }
+
+ /// Returns `None` for SDK events that have no Python class.
+ fn from_client(event: ClientCallEvent) -> Option {
+ match event {
+ ClientCallEvent::CallingStateChanged(state) => {
+ Some(Self::CallingStateChanged(CallingStateChanged {
+ state: state.into(),
+ }))
+ }
+ other => {
+ tracing::debug!(event = other.name(), "stream.rtc.events.unsupported");
+ None
+ }
+ }
+ }
+}
+
+/// The SDK receiver of one event stream.
+pub enum Source {
+ Sfu(Receiver),
+ Coordinator(Receiver),
+ Client(Receiver),
+}
+
+impl Source {
+ /// The next event, or `None` for an SDK event that has no Python class. A
+ /// receiver that fell behind gives `EventsLagged` and then continues.
+ async fn recv(&mut self, call_cid: &str) -> Result