feat: Expose live stream media info for the Stream Monitor UI#54
Merged
Conversation
Captures live ffmpeg progress (bitrate, fps, frame, speed) from each SharedTranscodingProcess's stderr by extending the line-buffer to split on \r as well as \n (ffmpeg writes its periodic stats line with a trailing \r so it overwrites in-place in a terminal — without the \r split they got buffered until the next newline arrived and we missed every update). On stream start we additionally fire-and-forget an ffprobe against the source URL so codec/container/resolution/audio get populated without delaying the start. The merged media_info dict is exposed in GET /streams alongside existing per-stream stats so the m3u-editor UI can render Dispatcharr-style badges. media_info is empty for plain HTTP-proxy streams since there's no live ffmpeg producer.
Running ffprobe against the source URL alongside ffmpeg doubled the upstream
connection count, which trips per-user concurrent-connection limits at IPTV
providers — the provider rejects the second connection (or both, looking like
a duplicate session) and ffmpeg dies seconds after starting. That broke
failover end-to-end: the old process was force-stopped on failover, the new
one would also die almost immediately, and the client connection timed out
before recovery.
ffmpeg already prints all the codec/container/resolution/audio info we need
in its own stderr at startup ("Input #0, FORMAT, from URL:" and "Stream #N:N:
Video|Audio: ..." lines). Parse those instead — zero extra connections, and
the data reflects what ffmpeg actually negotiated rather than what a separate
probe might see. Live progress (bitrate/fps/frame/speed) was already coming
from the periodic stats line, that path is unchanged.
…arted Two concurrent issues caused the freshly-started failover ffmpeg to die two seconds after it started, leaving the stream alive in name only (is_active true, no data flowing, media_info empty). 1. _try_update_failover_url awaited force_stop_stream before clearing stream_info.transcode_stream_key. While that await was in flight the client's streaming generator woke from failover_event, read the still-set key, and called get_or_create_shared_stream(reuse_stream_key=OLD_KEY, url=NEW_URL) — which detected url_changed, cleaned up the old process, and stood up a new SharedTranscodingProcess at the same key with the new URL. Clear the key first so concurrent readers see None and generate a fresh key from the new URL. 2. force_stop_stream took a reference to the old process, awaited per-client removal, then called _cleanup_local_process(stream_key) at the end. By the time that ran the dict could already hold the brand-new process the reconnecting client just installed at the same key, and we'd tear it down instead. Pop and capture up front; clean up the captured reference.
4 tasks
This was referenced May 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Surfaces live ffmpeg metadata (resolution, codec, fps, audio, container, bitrate, speed) on the
/streams/activeresponse so the m3u-editor Stream Monitor can overlay live values on top of its stored ffprobe-derived badges.Implements the proxy half of m3ue/m3u-editor#1085. Editor companion: m3ue/m3u-editor#1089.
What changed
SharedTranscodingProcessnow carries amedia_infodict that's populated from ffmpeg's own stderr — no extra ffprobe pass against the upstream URL, so we don't double the connection count and trip per-user limits at IPTV providers.Input #0, FORMAT, from URL:→containerStream #0:N: Video|Audio: codec, ..., 1920x1080, 50 fps, ...→video_codec,resolution,audio_codec,audio_channelsframe= ... fps= ... bitrate=...kbits/s speed=...x→ liveframe,fps,bitrate_kbps,speedStreamManager.get_stats()looks up the linked process and includesmedia_infoin each stream entry. Plain HTTP-proxy streams (no transcoding) return{}so the editor falls back to its stored probe data for those.Bonus: failover race fix
While testing this end-to-end I caught a latent race in the failover path that was killing the freshly-started transcoder ~2-3s after a successful failover swap.
_try_update_failover_urlwas clearingtranscode_stream_keyafter awaitingforce_stop_stream. During that await, a client coroutine could wake, read the still-set old key, and re-insert a brand-newSharedTranscodingProcessat that same key. Thenforce_stop_stream's tail-end cleanup popped and killed the new process by key — taking down the failover stream the user was trying to recover.Fix is two parts:
stream_info.transcode_stream_key = Nonebefore awaiting cleanup, so concurrent reconnects can't latch onto the doomed key.force_stop_streamnow pops + captures the process reference at the top and tears down that captured reference, regardless of what's at the key when the await chain unwinds.Test plan
/streams/activeshows populatedmedia_info(resolution, codec, fps, bitrate)media_info: {}media_inforepopulates from the failover streamtranscode_stream_keymismatches