Skip to content

docs: on_audio_ended / on_video_ended (session-end cleanup hooks) are undocumented #2406

@whitphx

Description

@whitphx

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:

  1. Allocate per-session state on first frame (a worker thread, a model handle, a queue, an st.session_state entry, etc.).
  2. 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

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions