Skip to content

feat: Expose live stream media info for the Stream Monitor UI#54

Merged
sparkison merged 4 commits into
m3ue:devfrom
Warbs816:feat/live-stream-media-info
May 2, 2026
Merged

feat: Expose live stream media info for the Stream Monitor UI#54
sparkison merged 4 commits into
m3ue:devfrom
Warbs816:feat/live-stream-media-info

Conversation

@Warbs816
Copy link
Copy Markdown
Contributor

@Warbs816 Warbs816 commented May 2, 2026

Summary

Surfaces live ffmpeg metadata (resolution, codec, fps, audio, container, bitrate, speed) on the /streams/active response 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

  • SharedTranscodingProcess now carries a media_info dict 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.
  • Three new parsers handle ffmpeg's three line shapes:
    • Input #0, FORMAT, from URL:container
    • Stream #0:N: Video|Audio: codec, ..., 1920x1080, 50 fps, ...video_codec, resolution, audio_codec, audio_channels
    • frame= ... fps= ... bitrate=...kbits/s speed=...x → live frame, fps, bitrate_kbps, speed
  • The first Video stream wins so secondary streams (e.g. embedded thumbnails) don't clobber the real codec. Live progress fields update freely.
  • StreamManager.get_stats() looks up the linked process and includes media_info in 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_url was clearing transcode_stream_key after awaiting force_stop_stream. During that await, a client coroutine could wake, read the still-set old key, and re-insert a brand-new SharedTranscodingProcess at that same key. Then force_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:

  1. Clear stream_info.transcode_stream_key = None before awaiting cleanup, so concurrent reconnects can't latch onto the doomed key.
  2. force_stop_stream now 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

  • Start a transcoded stream; within ~1-3s /streams/active shows populated media_info (resolution, codec, fps, bitrate)
  • Plain HTTP proxy stream returns media_info: {}
  • Trigger failover from the editor's Stream Monitor; new ffmpeg stays alive and media_info repopulates from the failover stream
  • Repeat failover quickly several times — no upstream reconnect storms, no stuck transcode_stream_key mismatches

Warbs816 added 3 commits May 2, 2026 01:16
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.
@sparkison sparkison merged commit 1fbc066 into m3ue:dev May 2, 2026
4 checks passed
@Warbs816 Warbs816 deleted the feat/live-stream-media-info branch May 2, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants