Zero-delay stream switching: prepare(next:) and seamless load - #31
Closed
bilipp wants to merge 2 commits into
Closed
Zero-delay stream switching: prepare(next:) and seamless load#31bilipp wants to merge 2 commits into
bilipp wants to merge 2 commits into
Conversation
PlayerSession gains waitForFirstFrame(timeout:) — the renderers accept media with the clock paused, so a freshly opened session reaches first-frame-decoded without ever playing; its display layer already shows a picture. state becomes publicly readable so consumers can check a standby session's health before adopting it. LumePlayer.prepare(next:) stages a URL in a fully independent standby session through that gate, and load(url:) swaps it in atomically (the old session tears down asynchronously — never blocking on thread joins). Without a prepared session, the new seamlessSwitching configuration option (default on) keeps the current session rendering while the replacement opens to its first frame, then swaps — channel zapping never shows a black gap. A failed seamless open retries cold, which covers providers that refuse a second concurrent connection. Generation counters make overlapping load/prepare/stop calls discard their superseded sessions instead of installing them. The session-epoch invariant is untouched: two sessions, one atomic promotion, no rebuild-in-place (PLAN.md §3.1, §6). Closes #16
IPTV accounts are often capped at one concurrent connection, and the overlapped switch holds two by design. seamlessSwitching becomes PlayerConfiguration.SwitchPolicy: .overlapped (unchanged default), .sequential, and .none (the old cold path). .sequential closes the current session *first* — a bounded join, so the connection is genuinely free before the replacement opens — while its display layer keeps showing the last decoded frame (SystemRenderer's shutdown flushes without removing the displayed image). The layers swap once the replacement holds its first frame: playback pauses during the switch, but the screen never blanks and at most one source connection exists at any moment. A failed overlapped open now retries sequentially instead of cold, keeping the frozen frame up during the retry. prepare(next:) documents that a standby costs the overlapped budget; consuming a prepared session stays allowed under any policy since the swap opens nothing new.
Owner
Author
|
Added single-connection support (6dacd95): |
bilipp
added a commit
to bilipp/Lume
that referenced
this pull request
Jul 13, 2026
LumeEngineCoordinator.configure now keeps the current session on screen while the replacement opens through its first decoded frame, then swaps the display layers atomically — channel surfing and episode changes never show a black gap. prepareNext(media:) stages the queued episode in a standby session so auto-advance lands on an already-decoded frame; PlayerNextUpOverlay fires a latched onPrepareNext hook ~30 s before the end (only when an advance is actually coming), wired up by LumeEngineEngineView. While a switch is in flight, the outgoing session's stall/failure events no longer schedule retries and its position no longer leaks into the new stream's clock. A failed seamless open falls back to the cold teardown-first path, which also covers providers that cap concurrent connections; reload() stays cold on purpose — stall recovery re-opens the same URL and must release the connection first. Generation counters let rapid zapping cancel in-flight switches and prepares cleanly. New "Seamless Switching" toggle (default on) in the Lume Engine options (iOS/macOS form + tvOS detail), persisted and covered by Restore Defaults. Requires the LumeEngine prepare/first-frame API (bilipp/LumeEngine#31).
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.
Closes #16.
Implements PLAN.md §6 zero-delay switching (roadmap P9): a second session opens through first-frame-decoded, then the renderer attachment swaps atomically; the old session tears down asynchronously.
What's in here
PlayerSession.waitForFirstFrame(timeout:)— the sample-buffer renderers accept media at rate 0, so a freshly opened session reaches first-frame-decoded with the clock paused; its display layer already shows a picture, which is what makes the swap gapless.statebecomes publicly readable so a standby session's health can be checked before adoption.LumePlayer.prepare(next:)— stages a URL in a fully independent standby session (own demuxer/decoders/renderer/layer) through the first-frame gate. Powers Lume's next-episode auto-advance.LumePlayer.load(url:)— three tiers: a prepared match swaps instantly; otherwise, with the newPlayerConfiguration.seamlessSwitchingoption (default on), the current session keeps rendering while the replacement opens to first frame, then swaps (channel zapping without a black gap); a failed seamless open retries cold — that covers IPTV providers that refuse a second concurrent connection.load/prepare/stopcalls safe: a superseded call discards its session instead of installing it or clobbering the winner's state.Invariants
Session epochs are untouched — there is no rebuild-in-place, just two independent sessions and one atomic promotion on the main actor.
LumePlayerViewneeded no changes: it re-installs the layer wheneverplayer.displayLayerchanges.Tests
Six new tests in
SeamlessSwitchingTests(first-frame gate on a never-played session, gate failure path, prepare→load atomic swap incl. "no second open" timing, seamless load without prepare, dead-source cold fallback ending in.failed, option-off cold path). Full suite: 69/69 passing.Consumer note
With the option on, a failed switch costs up to open-timeout + one cold retry before the error surfaces — the trade for never blanking the screen. The companion Lume PR wires this into channel zapping, next-episode prepare, and a settings toggle.