Seamless stream switching for the Lume Engine (zero-delay zap/advance) - #134
Closed
bilipp wants to merge 16 commits into
Closed
Seamless stream switching for the Lume Engine (zero-delay zap/advance)#134bilipp wants to merge 16 commits into
bilipp wants to merge 16 commits into
Conversation
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).
The overlapped seamless switch holds two provider connections, which a max_connections=1 account cannot serve — the second open gets refused or, worse, kills the playing stream. The coordinator now budgets per switch: overlapped only when the two streams belong to different playlists (they never contend), either side is a local file, or the account is known to allow at least two concurrent connections; otherwise a new sequential switch closes the outgoing session first (bounded join, so the connection is genuinely free), keeps its last decoded frame frozen on screen, and swaps layers once the replacement holds its first frame — one connection at any moment, and still no black gap. prepareNext is gated on the same overlap budget, since a standby session holds a connection of its own. A refused overlapped open retries sequentially before falling back cold. PlayableMedia carries the data: a stored maxConnections (from the owning playlist's Xtream max_connections, populated by the factories and forwarded through resuming/replacingURL) and a derived playlistID (the 36-char playlist-UUID prefix every catalog id embeds — the same recovery StalkerStreamResolver uses). Unknown caps (m3u, stalker, panels reporting "0") count as one connection: a wrong overlap can kill both streams, while a sequential switch only costs the moving picture during the switch. The switch flows move to LumeEngineCoordinator+Switching.swift (file length); the plumbing that mutates private(set) published state (beginSwitch, detachOutgoingSession, adopt, discardPreparedNext) stays on the class.
Owner
Author
|
Connection-aware update (12eb1b2): switching now never exceeds the playlist's connection cap.
Companion engine change: bilipp/LumeEngine#31 now ships Verified: macOS + tvOS builds, |
Explicit-self errors in Logger interpolation can't use self.prop — swiftformat's redundantSelf strips it and re-breaks the build.
…ng to System takes effect
feat(settings): add appearance override (System / Dark / Light)
A user's movie sync failed reproducibly: categories synced, then get_vod_streams was rejected through all three attempts and the whole content phase aborted. The launch EPG refresh was downloading xmltv.php from the same provider on a second connection (M3UClient has its own URLSession), tripping the one-connection account cap XtreamClient deliberately serializes around — and since neither the playlist nor the EPG schedule records a successful sync after the collision, both were due again on every launch, reproducing it forever. - AutoSync.blocksEPGRefresh: pure gate — a playlist blocks a background guide refresh while its content sync is running or due to start. - EPGSyncService.syncIfDue (launch) defers behind that gate; the new syncAfterContentSync (post-sync hook) ignores EPG dueness but defers while another queued playlist sync is pending. Manual "Sync Now" stays ungated. - Diagnostic logs: credential-free logDescription on XtreamError and M3UError, interpolated with privacy .public (the user's log only showed "<private>"), plus a new error line when Xtream retries exhaust — the final failure used to be silent. XtreamError moved to its own file (XtreamClient crossed the 600-line cap). - Tests: cover the new gate; the two EPGSyncSchedule tests now run in a serialized suite — they race each other via UserDefaults.standard.
fix(sync): stop the launch EPG refresh from racing the playlist sync
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).
The overlapped seamless switch holds two provider connections, which a max_connections=1 account cannot serve — the second open gets refused or, worse, kills the playing stream. The coordinator now budgets per switch: overlapped only when the two streams belong to different playlists (they never contend), either side is a local file, or the account is known to allow at least two concurrent connections; otherwise a new sequential switch closes the outgoing session first (bounded join, so the connection is genuinely free), keeps its last decoded frame frozen on screen, and swaps layers once the replacement holds its first frame — one connection at any moment, and still no black gap. prepareNext is gated on the same overlap budget, since a standby session holds a connection of its own. A refused overlapped open retries sequentially before falling back cold. PlayableMedia carries the data: a stored maxConnections (from the owning playlist's Xtream max_connections, populated by the factories and forwarded through resuming/replacingURL) and a derived playlistID (the 36-char playlist-UUID prefix every catalog id embeds — the same recovery StalkerStreamResolver uses). Unknown caps (m3u, stalker, panels reporting "0") count as one connection: a wrong overlap can kill both streams, while a sequential switch only costs the moving picture during the switch. The switch flows move to LumeEngineCoordinator+Switching.swift (file length); the plumbing that mutates private(set) published state (beginSwitch, detachOutgoingSession, adopt, discardPreparedNext) stays on the class.
…ub.com/bilipp/Lume into feature/lume-engine-seamless-switching
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.
Wires the engine's zero-delay switching (bilipp/LumeEngine#31, engine issue bilipp/LumeEngine#16) into the app. Merge the engine PR first — this branch calls the new
waitForFirstFrame/ public-stateAPI.What changes
LumeEngineCoordinator.configurenow keeps the current session on screen while the replacement opens through its first decoded frame, then swaps the display layers atomically (displayLayerbecame@Publishedso the video surface re-installs). The screen never goes black; a spinner shows over the still-playing outgoing stream.prepareNext(media:)stages the queued episode in a standby session through first-frame-decoded.PlayerNextUpOverlayfires a latchedonPrepareNexthook ~30 s before the end — only when premium + auto-advance/Next-button are actually enabled — so the advance lands on an already-decoded frame with zero delay. Other engines are unaffected (onPrepareNextdefaults tonil).reload()stays cold on purpose: stall recovery re-opens the same URL and must release the connection first.player.lume.seamlessSwitching, covered by Restore Defaults. The footer warns that a switch briefly holds two provider connections (the reason to turn it off on single-connection accounts).Trade-off to know
With the option on, a failed switch costs up to open-timeout + one cold retry before the failure overlay appears — the price of never blanking the screen.
Verification
Builds clean for macOS and tvOS Simulator against the local engine package; the engine-side behavior is covered by 6 new tests in LumeEngine (
SeamlessSwitchingTests, full suite 69/69).