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(