streamlit-webrtc: the camera toggle sends black frames, it does not close the session
Real-time video and audio processing on Streamlit
At a glance
- What is it?
- streamlit-webrtc puts a WebRTC session inside a Streamlit app with a frame callback, and its documentation is unusually honest about the two costs that come with that: the callback runs in its own thread, and the media toggle buttons mute the track rather than tearing it down. The dependency floors and the release script show a project that pins itself against Python 3.14 wheels.
- Who is it for?
- streamlit-webrtc is the right component when the work happens on frames inside your Python process, because the callback boundary is PyAV frames rather than a JavaScript buffer, and the streaming path is one function call. It is the wrong component when your processing belongs in the browser, when a camera switch has to end the transport rather than blank the picture, or when you cannot accept a forked callback thread with locks and a polling loop in your Streamlit script.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 9, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The media toggle mutes the track instead of stopping the session
When an app sends local camera or microphone input, `webrtc_streamer()` puts camera and microphone toggle buttons next to the Start and Stop button, and they let a user turn the outgoing track off without stopping the WebRTC session. What they do is set the track's enabled state. The documentation points at the MediaStreamTrack enabled property for the consequences: a disabled audio track sends silence and a disabled video track sends black frames, and the session does not stop and does not renegotiate. So the button is a content mute with the transport left running, which is the right trade for a demo and the wrong one for a privacy control. The buttons disappear with `media_toggle_controls=False`, which is worth knowing before you assume a user can turn their camera off at all.
The callback runs in a forked thread, and your script has to cope
The callback is not called on the main script's thread. It is executed in a forked thread running independently, and the documentation names two consequences you have to design around. First, thread safety: values passed between inside and outside the callback have to be thread-safe. Second, the main script stops at the bottom as usual while streaming, so you need a loop to keep the main script running and to poll the values the callback produced. `threading.Lock` is given as the standard way to control access across threads, and the worked example shares a mutable dict, `img_container`, between the callback and the outer scope while a loop computes a histogram from the frames. The page also points forward to a section on the limitations that multi-threading imposes on the callback, and the callback discussion itself ends inside that shared-container example.
The key argument is required, unlike other Streamlit components
The smallest app is four lines of import and one call, and the call is not interchangeable with other Streamlit components because the `key` argument is mandatory:
from streamlit_webrtc import webrtc_streamer
webrtc_streamer(key="sample")The documentation sets an arbitrary string in it and calls it a unique identifier. Install is one command, `$ pip install -U streamlit-webrtc`, and you launch with `$ streamlit run app.py` and open http://localhost:8501/. The first thing you see is the app view and a START button; pressing it starts video and audio streaming, and if the browser asks for camera and microphone permission you allow it. From there, `video_frame_callback` or `audio_frame_callback` receives and returns PyAV frames, so `frame.to_ndarray(format="bgr24")` and `av.VideoFrame.from_ndarray(flipped, format="bgr24")` are the two ends of the transform.
Three dependency floors are all about Python 3.14 wheels
The runtime dependency block explains itself in comments, and the reasoning is wheel availability rather than feature need. `requires-python` is 3.10 or newer, and the streamlit floor of 1.51.0 is annotated as the first Streamlit release that requires Python 3.10, set because Python 3.9 reached end of life on 2025-10-31. The aiortc floor is 1.14.0, and the comment says the cryptography error was fixed back in 1.4.0, but anything below 1.14.0 caps av below 15, which lacks Python 3.14 wheels. The av floor is 15.1.0, annotated as the first version with prebuilt cp314 wheels. The remaining runtime entries are aioice at 0.10.1 or newer and packaging at 20.0 or newer. Read together, the three floors are one decision: do not let a resolver pick a WebRTC stack that cannot be installed on a current interpreter.
The version lives in the tag, and a script checks the other copy
The package version is dynamic and comes from `source = "vcs"`, so the number in a wheel is the git tag. A second copy lives in `streamlit_webrtc/component.py`, and the build target starts by running a check against it:
python scripts/release_check.py streamlit_webrtc/component.py
cd streamlit_webrtc/frontend && pnpm run build
uv buildSo a release is a three step chain: verify the checked in version, compile the frontend with pnpm, then build the distributions. The release target itself is a bump-my-version call that tags, commits with `--commit-args='--allow-empty'`, and then runs `git push` followed by `git push --tags`. Patch, minor and major targets differ only in the version argument. The recent tags show the shape: v0.77.0 on 7 August 2026, then v0.78.0 and v0.78.1 on the same day, 18 September, six hours apart.
Two linters are configured and only one is wired up
The repository carries a `.flake8` file at the root while the dev dependency group and the Makefile both name ruff. `format/backend` runs `uv run ruff format .` and then `uv run ruff check . --fix`, and `format/frontend` shells into the frontend directory to run `pnpm format`; the top level `format` target calls both. Nothing in the Makefile invokes flake8, so the configuration file is a leftover that will not complain about anything. The rest of the toolchain is a normal modern Python setup: pytest and pytest-asyncio for tests, mypy with the faster cache extra, pre-commit with its own config file, mkdocs-material for the documentation site, and scriv for the changelog fragments that accumulate in `changelog.d/` beside `CHANGELOG.md`. The dev group also pins `audioop-lts==0.2.2` only for Python 3.13 and newer, which is the interpreter that removed audioop from the standard library.
The sdist includes the frontend the exclude list names
The packaging target draws a line around what leaves the repository. The sdist includes `/streamlit_webrtc` and excludes `/streamlit_webrtc/frontend`, then re-includes the frontend with a negated entry, so the built component carries the browser side it needs rather than a stub. The dev group backs that up with an `openai[realtime]` dependency annotated for one specific sample, `pages/18_audio_chat_realtime.py`, with a comment that the realtime extra pulls in websockets. The rest of the tree explains what is deliberately not packaged: `refactoring/`, `sample_utils/`, `scripts/`, `docs/` with `mkdocs.yml`, `pages/`, `tests/` and the two example apps `app_deepspeech.py` and `app_videochat.py` sit at the root, alongside `home.py` and a committed `uv.lock`.
The examples are separate apps, and one of them moved to Wasm
The example list is a set of sibling repositories rather than sample pages, and the root of this one only holds two of them. The showcase covers object detection, an OpenCV filter, uni-directional video streaming and audio processing. The speech-to-text app is described as self-contained, with no external API, which is the pattern to copy if your audio path should not leave the process. A style transfer app applies filters to a live stream, a video chat example claims about a hundred lines of Python and has no online demo, and the Tokyo 2020 pictogram example uses MediaPipe for pose estimation. The sister project `streamlit-fesion` exists to run video filters in the browser with Wasm instead, which is the clearest statement of where this library stops.
Editorial conclusion
streamlit-webrtc is the right component when the work happens on frames inside your Python process, because the callback boundary is PyAV frames rather than a JavaScript buffer, and the streaming path is one function call. It is the wrong component when your processing belongs in the browser, when a camera switch has to end the transport rather than blank the picture, or when you cannot accept a forked callback thread with locks and a polling loop in your Streamlit script. Before you build on it, read the media toggle behaviour and decide whether silence and black frames are an acceptable state for your users, pin aiortc and av at or above the stated floors so a resolver cannot walk them back, keep the dev group's Python 3.13 audioop backport in mind if you target 3.13, and expect to run the release script, not the Makefile alone, whenever the tag and the version in component.py have to agree.
Frequently asked questions
what is streamlit webrtc
It is a Streamlit component for handling and transmitting real-time video and audio streams over the network. `webrtc_streamer()` takes a required `key` argument, shows a START button, asks the browser for camera and microphone access, and hands frames to `video_frame_callback` or `audio_frame_callback` as PyAV frames that you convert with `to_ndarray` and rebuild with `from_ndarray`.
How do I install streamlit-webrtc?
Run `pip install -U streamlit-webrtc`. The runtime dependencies are streamlit, aiortc, av, aioice and packaging, the package requires Python 3.10 or newer, and its version is taken from the git tags rather than from a literal in the manifest.
Why does streamlit-webrtc require aiortc 1.14 or newer?
The manifest comment says aiortc below 1.4.0 raises an error with cryptography 39 or newer, and that aiortc below 1.14.0 caps av below 15, which has no Python 3.14 wheels. That is why av itself is floored at 15.1.0, the first version with prebuilt cp314 wheels.
Does turning the camera off in streamlit-webrtc stop the stream?
No. The toggle buttons set the track's enabled state, and the documentation notes that a disabled audio track sends silence and a disabled video track sends black frames while the session neither stops nor renegotiates. The buttons can be removed from the interface with `media_toggle_controls=False`.
How does threading work in a streamlit-webrtc frame callback?
The callback executes in a forked thread running independently of the main script, so values crossing the boundary have to be thread-safe, and the main script needs a loop to keep running and poll what the callback produced. The documentation uses `threading.Lock` with a shared mutable dict as the worked pattern.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/whitphx-streamlit-webrtc)