From 4ec9b936a6e4db1f76b2467bc668556da7e63775 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 18 Jul 2026 16:28:19 +0200 Subject: [PATCH] Fix background playback setting behavior across platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hide the toggle on macOS, where apps are never suspended and playback always continues regardless of the setting. Pause playback on iOS when backgrounding with the setting disabled — the audio background mode previously kept mpv playing, making the off state a no-op. Move the continue-playing policy into PlayerService and make the backend's background render pause unconditional (PiP-gated only): it is a GPU optimization that should not depend on the setting. Also ungate the player sheet visibility handlers, which manage foreground mini player rendering unrelated to background playback. --- Yattee/Services/Player/MPVBackend.swift | 6 ++-- Yattee/Services/Player/PlayerBackend.swift | 7 +++-- Yattee/Services/Player/PlayerService.swift | 29 ++++++++++++------- Yattee/Views/Player/MPVVideoView.swift | 2 +- .../Views/Settings/PlaybackSettingsView.swift | 4 +++ 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Yattee/Services/Player/MPVBackend.swift b/Yattee/Services/Player/MPVBackend.swift index a2c118dc..f89304f3 100644 --- a/Yattee/Services/Player/MPVBackend.swift +++ b/Yattee/Services/Player/MPVBackend.swift @@ -998,7 +998,7 @@ final class MPVBackend: PlayerBackend { // MARK: - Background Playback - func handleScenePhase(_ phase: ScenePhase, backgroundEnabled: Bool, isPiPActive: Bool) { + func handleScenePhase(_ phase: ScenePhase, isPiPActive: Bool) { #if os(iOS) let pipActive = self.isPiPActive || isPiPActive #else @@ -1008,8 +1008,8 @@ final class MPVBackend: PlayerBackend { MPVLogging.logAppLifecycle("handleScenePhase(\(phase))", isPiPActive: pipActive, isRendering: nil) - guard backgroundEnabled, !pipActive else { - MPVLogging.log("handleScenePhase: skipping (bgEnabled:\(backgroundEnabled) pip:\(pipActive))") + guard !pipActive else { + MPVLogging.log("handleScenePhase: skipping (pip:\(pipActive))") return } diff --git a/Yattee/Services/Player/PlayerBackend.swift b/Yattee/Services/Player/PlayerBackend.swift index 98c8b6c4..ac27e201 100644 --- a/Yattee/Services/Player/PlayerBackend.swift +++ b/Yattee/Services/Player/PlayerBackend.swift @@ -249,11 +249,12 @@ protocol PlayerBackend: AnyObject { // MARK: - Background Playback /// Handle scene phase changes for background playback. + /// Backends should pause rendering while the app is in the background; + /// whether playback itself continues is decided by PlayerService. /// - Parameters: /// - phase: The new scene phase - /// - backgroundEnabled: Whether background playback is enabled in settings /// - isPiPActive: Whether Picture-in-Picture is currently active - func handleScenePhase(_ phase: ScenePhase, backgroundEnabled: Bool, isPiPActive: Bool) + func handleScenePhase(_ phase: ScenePhase, isPiPActive: Bool) } // MARK: - Default Implementations @@ -278,7 +279,7 @@ extension PlayerBackend { } /// Default implementation does nothing. - func handleScenePhase(_ phase: ScenePhase, backgroundEnabled: Bool, isPiPActive: Bool) { + func handleScenePhase(_ phase: ScenePhase, isPiPActive: Bool) { // No-op by default } } diff --git a/Yattee/Services/Player/PlayerService.swift b/Yattee/Services/Player/PlayerService.swift index caee4384..84b67ec3 100644 --- a/Yattee/Services/Player/PlayerService.swift +++ b/Yattee/Services/Player/PlayerService.swift @@ -681,14 +681,23 @@ final class PlayerService { } #endif - let backgroundEnabled = settingsManager?.backgroundPlaybackEnabled ?? true #if os(iOS) let isPiPActive = (currentBackend as? MPVBackend)?.isPiPActive ?? false #else let isPiPActive = false #endif - #if os(tvOS) + // With background playback disabled, pause when the app leaves the + // foreground — the audio background mode would otherwise keep mpv + // playing. Not applicable on macOS, where apps are never suspended. + #if os(iOS) + let backgroundEnabled = settingsManager?.backgroundPlaybackEnabled ?? true + if phase == .background, !backgroundEnabled, !isPiPActive, state.playbackState == .playing { + LoggingService.shared.debug("PlayerService: pausing playback (phase=\(phase), backgroundPlaybackEnabled=false)", category: .player) + pause() + } + #elseif os(tvOS) + let backgroundEnabled = settingsManager?.backgroundPlaybackEnabled ?? true LoggingService.shared.debug("PlayerService[tvOS-bg]: phase=\(phase) bgEnabled=\(backgroundEnabled) playbackState=\(state.playbackState)", category: .player) if (phase == .background || phase == .inactive), !backgroundEnabled, state.playbackState == .playing { LoggingService.shared.debug("PlayerService[tvOS-bg]: pausing playback (phase=\(phase), backgroundPlaybackEnabled=false)", category: .player) @@ -696,7 +705,7 @@ final class PlayerService { } #endif - currentBackend?.handleScenePhase(phase, backgroundEnabled: backgroundEnabled, isPiPActive: isPiPActive) + currentBackend?.handleScenePhase(phase, isPiPActive: isPiPActive) } /// Plays the next video in queue, respecting the current queue mode. @@ -2483,12 +2492,11 @@ final class PlayerService { LoggingService.shared.debug("PlayerService: playerSheetDidAppear - backend=\(backendType), playbackState=\(playbackState)", category: .player) // Re-enable visual tracks and reattach layer when sheet appears - let backgroundEnabled = settingsManager?.backgroundPlaybackEnabled ?? true let isPiPActive = (currentBackend as? MPVBackend)?.isPiPActive ?? false - LoggingService.shared.debug("PlayerService: sheetDidAppear checks - backgroundEnabled=\(backgroundEnabled), isPiPActive=\(isPiPActive)", category: .player) + LoggingService.shared.debug("PlayerService: sheetDidAppear checks - isPiPActive=\(isPiPActive)", category: .player) - guard backgroundEnabled && !isPiPActive else { - LoggingService.shared.debug("PlayerService: skipping visibility handling (backgroundEnabled=\(backgroundEnabled), isPiPActive=\(isPiPActive))", category: .player) + guard !isPiPActive else { + LoggingService.shared.debug("PlayerService: skipping visibility handling (isPiPActive=\(isPiPActive))", category: .player) return } @@ -2505,12 +2513,11 @@ final class PlayerService { LoggingService.shared.debug("PlayerService: playerSheetDidDisappear - backend=\(backendType), playbackState=\(playbackState)", category: .player) // Disable visual tracks and detach layer for background audio playback - let backgroundEnabled = settingsManager?.backgroundPlaybackEnabled ?? true let mpvPiPActive = (currentBackend as? MPVBackend)?.isPiPActive ?? false - LoggingService.shared.debug("PlayerService: sheetDidDisappear checks - backgroundEnabled=\(backgroundEnabled), mpvPiPActive=\(mpvPiPActive)", category: .player) + LoggingService.shared.debug("PlayerService: sheetDidDisappear checks - mpvPiPActive=\(mpvPiPActive)", category: .player) - guard backgroundEnabled && !mpvPiPActive else { - LoggingService.shared.debug("PlayerService: skipping visibility handling on disappear (backgroundEnabled=\(backgroundEnabled), mpvPiP=\(mpvPiPActive))", category: .player) + guard !mpvPiPActive else { + LoggingService.shared.debug("PlayerService: skipping visibility handling on disappear (mpvPiP=\(mpvPiPActive))", category: .player) return } diff --git a/Yattee/Views/Player/MPVVideoView.swift b/Yattee/Views/Player/MPVVideoView.swift index 6c326c54..6bea515a 100644 --- a/Yattee/Views/Player/MPVVideoView.swift +++ b/Yattee/Views/Player/MPVVideoView.swift @@ -146,7 +146,7 @@ struct MPVVideoView: View { #if os(iOS) || os(macOS) // Resume the MPV display link when the expanded video view appears. // MiniPlayerView pauses rendering when its video preview hides during expand; - // PlayerService.playerSheetDidAppear is gated behind background playback, so it + // PlayerService.playerSheetDidAppear skips resuming while PiP is active, so it // can't be relied on to resume. On macOS this also recovers the sheet re-open // black screen when the shared render view reappears without a window re-parent. backend.resumeRendering() diff --git a/Yattee/Views/Settings/PlaybackSettingsView.swift b/Yattee/Views/Settings/PlaybackSettingsView.swift index 2420fabb..ea314acc 100644 --- a/Yattee/Views/Settings/PlaybackSettingsView.swift +++ b/Yattee/Views/Settings/PlaybackSettingsView.swift @@ -270,10 +270,14 @@ private struct BehaviorSection: View { } } + #if !os(macOS) + // macOS never suspends apps, so playback always continues in the + // background — the toggle would have no effect there Toggle( String(localized: "settings.playback.backgroundPlayback"), isOn: $settings.backgroundPlaybackEnabled ) + #endif #if os(tvOS) Toggle(