Skip to content

Broadcast: audio stream tests + subtitle/seek drift correction#60

Open
Grimothy wants to merge 6 commits into
m3ue:masterfrom
Grimothy:master
Open

Broadcast: audio stream tests + subtitle/seek drift correction#60
Grimothy wants to merge 6 commits into
m3ue:masterfrom
Grimothy:master

Conversation

@Grimothy

@Grimothy Grimothy commented Jul 8, 2026

Copy link
Copy Markdown

Summary

Broadcast subsystem: audio stream handling tests and subtitle/seek drift correction. Pairs with m3u-editor single-authority seek work.

Commits

  • `eecbf26` test(broadcast): add broadcast audio stream tests
  • `635d730` feat(api): extend BroadcastStartRequest with subtitle/seek/audio stream fields
  • `bce4588` fix(broadcast): add -itsoffset drift correction for #range= byte-seek subtitles
  • `50c28de` feat: include hls_dir in broadcast status response for DVR post-processing (reverted in `9569c5d` — not needed in prod)

Notes

  • Subtitle drift fix pairs with editor-side `getSubtitleUrl` `server_seeked` flag.
  • The hls_dir commit and its revert are both included; net effect is no hls_dir change.

…ssing

- Add hls_dir field to BroadcastStatus dataclass and get_status() return
- Add hls_dir to BroadcastStatusResponse Pydantic model
- Pass hls_dir through in GET /broadcast/{network_id}/status route
- Include /dvr/recordings/ in DVR URL pattern matching
… subtitles

When the video URL uses ffmpeg's `#range=<n>-` byte-seek URL suffix against
Emby's static endpoint (which doesn't advertise duration metadata), ffmpeg
waits for the next GOP boundary before emitting the first video packet. The
subtitle URL is independently server-pre-seeked via Emby's startPositionTicks
path segment so its cues land at PTS 0 — meaning subtitles land ~1-3s AHEAD
of the video's first PTS, leaving the user with a "tad slow or fast" desync.

- Add `_probe_video_first_pts()`: writes a 0.5s TS chunk to a tmpfile via ffmpeg,
  then runs ffprobe on the file to extract the real first-packet PTS. Cached
  per BroadcastProcess instance so it's only paid for once per broadcast.
- In `_build_ffmpeg_command()`: when `seek_seconds > 0` (byte-seek path) and
  subtitles are active, probe and apply `-itsoffset +<PTS>` to the subtitle
  input to shift cues forward to the video's GOP start.
- Read 1.5s probe budget added to broadcast startup (acceptable for live
  broadcast start which is already slow).
- 2 new tests: drift_correction applies with probed PTS > 0.1s, and skips when
  probe returns 0 (no_seek or pure_server_transcode path).
- Refactor 1 existing test to mock the 2-step subprocess.run (ffmpeg + ffprobe)
  instead of the original single-ffprobe assumption.

Empirically measured drift: 1.400s on item 817 (Night at the Museum), 1.483s
on item 437 (Interstellar) — bounded by GOP size, typically < 3s for HEVC.

Live validation: test 2 broadcast with seek=3500 produced subtitle cues
at PTS 4.124s (rebased cue offset + 1.4s drift correction), versus 2.724s
without correction — exactly aligned with the video's GOP-boundary start.
…am fields

Mirror the BroadcastConfig dataclass fields added in earlier work so the
HTTP boundary accepts the same payload:

- audio_stream_index: optional preferred-language audio stream index
  (resolved by Laravel from the media server metadata).
- subtitles_enabled: request the proxy to detect/expose embedded subtitle
  tracks as a toggleable WebVTT rendition in the HLS output.
- subtitle_url + subtitle_language: explicit subtitle URL resolved by
  Laravel from the media server metadata (covers embedded AND external
  sidecar-file subtitles).
- subtitle_seek_seconds: independent seek offset for the subtitle URL.
  0 means the subtitle is already server-pre-seeked (Emby startPositionTicks
  rebased the cues); > 0 means seek locally with -ss and correct via
  -itsoffset.

The next_stream_config mirror lets the proxy pre-queue auto-transition
without waiting for a Laravel round-trip.
Tests for the audio_stream_index field flow in BroadcastConfig: ensure
the proxy honors the explicit audio stream index when set, and falls
back to the default (first audio) stream when not set.
# Conflicts:
#	src/broadcast_manager.py
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.

1 participant