Summary
webrtc_streamer accepts on_audio_ended / on_video_ended callbacks (MediaEndedCallback = Callable[[], None]), and *ProcessorBase exposes an on_ended() method that fires when the inbound media track ends. In practice these are the canonical hooks for tearing down user-managed resources (worker threads, GPU pipelines, queues, ML model state, etc.) when the user stops a WebRTC session — but they are not documented or demonstrated anywhere I can find:
- README.md — searched for
on_audio_ended / on_video_ended / on_ended: no hits beyond a generic note about "main script stops at the bottom".
pages/ examples — searched all 15 pages for on_audio_ended / on_video_ended / on_ended: zero matches. None of the worked examples (10_sendonly_audio.py, 9_sendonly_video.py, 1_object_detection.py, 14_programmable_source.py, 15_audio_source.py, etc.) demonstrate cleanup.
Why it matters
When using audio_frame_callback / video_frame_callback (the simple, non-processor pattern), a user typically wants to:
- Allocate per-session state on first frame (a worker thread, a model handle, a queue, an
st.session_state entry, etc.).
- Tear that state down when the user clicks Stop / closes the tab / the network drops.
Without knowing about on_audio_ended / on_video_ended, the only obvious lifecycle hook is on_change — but on_change fires on any state change (signalling, playing) and the user has to dig through the source to figure out the right boolean predicate (not playing and not signalling). It's also a state-driven hook rather than an event-driven one, which makes it less obvious that the right place to clean up is "right now".
The right answer (on_audio_ended for SENDONLY audio capture, on_video_ended for SENDONLY video capture) only surfaces if you read streamlit_webrtc/component.py and streamlit_webrtc/process.py to follow the call chain MediaProcessTrack.stop() → processor.on_ended() → CallbackAttachableProcessor._media_ended_callback → user callback.
Suggested doc additions
-
README: a "Session lifecycle" / "Cleanup on Stop" section that names on_audio_ended / on_video_ended, explains when they fire, and shows a minimal cleanup snippet for st.session_state-stored resources.
-
A new example page: pages/N_session_lifecycle.py that puts a long-lived resource (e.g. a daemon worker thread, or a counter) behind the audio/video callback, prints started / frame received / stopped events, and demonstrates that on_audio_ended fires deterministically when the user clicks Stop. This would also visualise the relationship between on_change and on_*_ended.
-
API reference: extend the docstrings of webrtc_streamer's on_audio_ended / on_video_ended parameters to (a) link them to ProcessorBase.on_ended and (b) explicitly state that they're the recommended cleanup hook for the callback-style API. Mention that the firing thread is not the Streamlit main thread (it's aiortc's asyncio loop), so user code should avoid heavy / Streamlit-specific work and stick to thread-safe state mutations (signaling events, dropping st.session_state keys, etc.).
Asymmetric source/sink pattern (related, lower priority)
Beyond the basic cleanup story, the asymmetric "input one modality, output another" pattern (e.g. audio in → video out via audio_frame_callback + create_video_source_track) isn't represented in the examples either. 14_programmable_source.py is RECVONLY video-only; 15_audio_source.py is RECVONLY audio-only. A combined example (one peer connection for inbound, another for outbound, with desired_playing_state tying them together) would document a non-obvious but useful integration pattern that I just worked through for an ARTalk realtime demo. Happy to contribute the example back if the shape is right.
Context
Filed alongside #2405 (the AudioSourceTrack time_base bug), found while debugging the same downstream app. Marking this docs-only since the runtime behaviour of the hooks is correct — only the discoverability is the issue.
Summary
webrtc_streameracceptson_audio_ended/on_video_endedcallbacks (MediaEndedCallback = Callable[[], None]), and*ProcessorBaseexposes anon_ended()method that fires when the inbound media track ends. In practice these are the canonical hooks for tearing down user-managed resources (worker threads, GPU pipelines, queues, ML model state, etc.) when the user stops a WebRTC session — but they are not documented or demonstrated anywhere I can find:on_audio_ended/on_video_ended/on_ended: no hits beyond a generic note about "main script stops at the bottom".pages/examples — searched all 15 pages foron_audio_ended/on_video_ended/on_ended: zero matches. None of the worked examples (10_sendonly_audio.py,9_sendonly_video.py,1_object_detection.py,14_programmable_source.py,15_audio_source.py, etc.) demonstrate cleanup.Why it matters
When using
audio_frame_callback/video_frame_callback(the simple, non-processor pattern), a user typically wants to:st.session_stateentry, etc.).Without knowing about
on_audio_ended/on_video_ended, the only obvious lifecycle hook ison_change— buton_changefires on any state change (signalling, playing) and the user has to dig through the source to figure out the right boolean predicate (not playing and not signalling). It's also a state-driven hook rather than an event-driven one, which makes it less obvious that the right place to clean up is "right now".The right answer (
on_audio_endedfor SENDONLY audio capture,on_video_endedfor SENDONLY video capture) only surfaces if you readstreamlit_webrtc/component.pyandstreamlit_webrtc/process.pyto follow the call chainMediaProcessTrack.stop()→processor.on_ended()→CallbackAttachableProcessor._media_ended_callback→ user callback.Suggested doc additions
README: a "Session lifecycle" / "Cleanup on Stop" section that names
on_audio_ended/on_video_ended, explains when they fire, and shows a minimal cleanup snippet forst.session_state-stored resources.A new example page:
pages/N_session_lifecycle.pythat puts a long-lived resource (e.g. a daemon worker thread, or a counter) behind the audio/video callback, printsstarted/frame received/stoppedevents, and demonstrates thaton_audio_endedfires deterministically when the user clicks Stop. This would also visualise the relationship betweenon_changeandon_*_ended.API reference: extend the docstrings of
webrtc_streamer'son_audio_ended/on_video_endedparameters to (a) link them toProcessorBase.on_endedand (b) explicitly state that they're the recommended cleanup hook for the callback-style API. Mention that the firing thread is not the Streamlit main thread (it's aiortc's asyncio loop), so user code should avoid heavy / Streamlit-specific work and stick to thread-safe state mutations (signaling events, droppingst.session_statekeys, etc.).Asymmetric source/sink pattern (related, lower priority)
Beyond the basic cleanup story, the asymmetric "input one modality, output another" pattern (e.g. audio in → video out via
audio_frame_callback+create_video_source_track) isn't represented in the examples either.14_programmable_source.pyis RECVONLY video-only;15_audio_source.pyis RECVONLY audio-only. A combined example (one peer connection for inbound, another for outbound, withdesired_playing_statetying them together) would document a non-obvious but useful integration pattern that I just worked through for an ARTalk realtime demo. Happy to contribute the example back if the shape is right.Context
Filed alongside #2405 (the
AudioSourceTracktime_base bug), found while debugging the same downstream app. Marking this docs-only since the runtime behaviour of the hooks is correct — only the discoverability is the issue.