Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
38d0e34
feat: bind the Rust Video SDK as a native module
dangusev Sep 29, 2026
11c7380
rust: release GIL before accessing inner wrapper state in Call and Lo…
dangusev Sep 29, 2026
603a477
rust: route wrapper logs as internal SDK logs
dangusev Sep 29, 2026
3caea52
agents.md: add more instructions for Rust and PyO3 wrapping
dangusev Sep 29, 2026
92a006e
rust: convert _native to a module
dangusev Sep 29, 2026
276a35c
rust: move _native_logging to _native/logging
dangusev Sep 29, 2026
0b6f455
rust: rename _native to _rust, make bindings a core module
dangusev Sep 29, 2026
7c9ee38
rust: expose wrapped Rust SDK errors as exceptions
dangusev Sep 29, 2026
4e4c859
rust: forward SDK logs in tests
dangusev Sep 29, 2026
882ba67
rust: use one session fixture for call users
dangusev Sep 29, 2026
198edc1
rust: bump Rust SDK revision
dangusev Sep 29, 2026
5b56f83
rust:set module for pyclasses to have normal path instead of "builtins"
dangusev Sep 29, 2026
182bef6
rust: bind stop_publish, screen share and pcm_queue_capacity
dangusev Sep 30, 2026
a428cdf
rust: rewire ConnectionManager onto the Rust SDK
dangusev Oct 2, 2026
efed647
rtc: re-export track and event classes from getstream.video.rtc
dangusev Oct 2, 2026
7274bc2
rust: bump Rust SDK revision to 63e5fa6
dangusev Oct 5, 2026
b19928b
rtc: replace the published track of the same kind in add_tracks
dangusev Oct 5, 2026
0f5d1ba
rust: bump Rust SDK revision to 77f8dd3
dangusev Oct 5, 2026
73cbcd6
rtc: remove the aiortc transport code from ConnectionManager
dangusev Oct 5, 2026
4811651
rtc: delete the aiortc transport modules
dangusev Oct 5, 2026
ad8a827
deps: remove unused packages from the webrtc extra
dangusev Oct 5, 2026
7bef818
rust: bump Rust SDK revision to b7bb737
dangusev Oct 5, 2026
971a88f
scripts: delete the aiortc SFU connect script
dangusev Oct 5, 2026
11d9c27
examples: add an echo agent that uses the public rtc API
dangusev Oct 5, 2026
a3ec103
rtc: end the forwarding when the SDK stops the published track
dangusev Oct 5, 2026
68cef33
examples: add an audio recorder that uses the public rtc API
dangusev Oct 5, 2026
11d2ec4
rtc: fix the type check errors
dangusev Oct 5, 2026
99e603d
rust: bump Rust SDK revision to dbfd13c
dangusev Oct 5, 2026
566045f
rust: bump Rust SDK revision to ef8694d
dangusev Oct 6, 2026
6ba558b
rust: bump Rust SDK revision to f19b7e4
dangusev Oct 7, 2026
f2b8acb
rtc: move the Rust extension into the getstream-rtc package
dangusev Oct 7, 2026
259bef3
docs: describe the getstream-rtc package in AGENTS.md
dangusev Oct 7, 2026
bc67bb1
tests: move the binding tests into getstream-rtc
dangusev Oct 7, 2026
5f5837b
ci: build getstream-rtc only in the video and Rust jobs
dangusev Oct 7, 2026
4c511a7
rtc: resample with the SDK StreamResampler
dangusev Oct 9, 2026
855e455
chore: update migration.md and uv.lock
dangusev Oct 9, 2026
78c19f7
rtc: end streams created after leave() at once
dangusev Oct 9, 2026
5a0fedc
rtc: start log forwarding on first use and configure it off the loop
dangusev Oct 9, 2026
b1decaa
rtc: log the failure of a publish forwarder
dangusev Oct 9, 2026
20f399c
rtc: start no publish forwarder after leave() has begun
dangusev Oct 9, 2026
8447041
tests, docs: fix the custom event test and describe the fork limit
dangusev Oct 9, 2026
7a442c4
rust: bump Rust SDK revision to c865308
dangusev Oct 13, 2026
e5a4574
docs: compare migration.md with 6.1.1 and describe SDK c865308
dangusev Oct 14, 2026
651352a
rtc: handle a track queue overflow and a failed leave()
dangusev Oct 14, 2026
3379873
ci: do not keep the checkout credential in the test jobs
dangusev Oct 14, 2026
ec3fa04
chore: run the getstream-rtc tests in make test-all
dangusev Oct 14, 2026
13e0de2
chore: fix the protocol folder comment in .gitignore
dangusev Oct 14, 2026
ee0dcc3
chore: undo the dependency upgrade and keep ruff below 0.16
dangusev Oct 14, 2026
971a3b3
examples: load .env and stop quietly on Ctrl+C
dangusev Oct 14, 2026
0ab2b18
rtc: update example page url in open_browser()
dangusev Oct 14, 2026
939137a
examples: select the webrtc extra in the run commands
dangusev Oct 14, 2026
b623b98
rtc: fix the participants after a rejoin and the call_ended reason
dangusev Oct 14, 2026
0ada604
rtc: document that a revoked grant stops or mutes a published track
dangusev Oct 15, 2026
3e1568e
rtc: deliver Rust log records from a Python thread
dangusev Oct 15, 2026
0951fee
rtc: parse coordinator events with the same values and key order as j…
dangusev Oct 15, 2026
c71f15e
rtc: log SDK events that have no Python class
dangusev Oct 15, 2026
70b5a5d
rtc: pin the Rust SDK to a commit on main
dangusev Oct 15, 2026
16aa3c2
rtc: convert unknown participant enum values as the SDK does
dangusev Oct 16, 2026
189cbea
rtc: test the other JSON kinds of coordinator event data
dangusev Oct 16, 2026
7c7d98f
docs: update migration.md for the review fixes
dangusev Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .github/actions/python-uv-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' || '' }}
5 changes: 5 additions & 0 deletions .github/workflows/run_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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:
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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.
Expand All @@ -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

Expand All @@ -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:
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
cursor[bot] marked this conversation as resolved.
# Local protocol folder (handled by generate_webrtc.sh)
protocol/
94 changes: 92 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.<area>.<event>`. 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)`.
7 changes: 3 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
Loading
Loading