Expose Background Playback toggle on tvOS, default off

Surfaces the existing iOS/macOS Background Playback setting in the tvOS
Playback settings, defaulting to off so audio stops when the user leaves
the app via the TV button. Pauses playback on .background/.inactive when
the toggle is off, regardless of audio route — the user's setting wins
over AirPlay/HomePod handoff. Also auto-shows the tvOS player controls
when returning to foreground so the paused state is immediately visible
and actionable.
This commit is contained in:
Arkadiusz Fal
2026-05-09 14:50:38 +02:00
parent 8a3f76bb1d
commit aabf5313fa
4 changed files with 26 additions and 3 deletions

View File

@@ -40,7 +40,7 @@ extension SettingsManager {
var backgroundPlaybackEnabled: Bool { var backgroundPlaybackEnabled: Bool {
get { get {
if let cached = _backgroundPlaybackEnabled { return cached } if let cached = _backgroundPlaybackEnabled { return cached }
return bool(for: .backgroundPlayback, default: true) return bool(for: .backgroundPlayback, default: backgroundPlaybackDefault)
} }
set { set {
_backgroundPlaybackEnabled = newValue _backgroundPlaybackEnabled = newValue
@@ -48,6 +48,14 @@ extension SettingsManager {
} }
} }
private var backgroundPlaybackDefault: Bool {
#if os(tvOS)
return false
#else
return true
#endif
}
/// tvOS only: when enabled, the Siri remote Menu button closes the video /// tvOS only: when enabled, the Siri remote Menu button closes the video
/// (clears queue, stops playback) instead of only collapsing the player. /// (clears queue, stops playback) instead of only collapsing the player.
/// When enabled, the explicit top-bar close button is hidden. /// When enabled, the explicit top-bar close button is hidden.

View File

@@ -650,6 +650,15 @@ final class PlayerService {
#else #else
let isPiPActive = false let isPiPActive = false
#endif #endif
#if os(tvOS)
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)
pause()
}
#endif
currentBackend?.handleScenePhase(phase, backgroundEnabled: backgroundEnabled, isPiPActive: isPiPActive) currentBackend?.handleScenePhase(phase, backgroundEnabled: backgroundEnabled, isPiPActive: isPiPActive)
} }

View File

@@ -31,6 +31,7 @@ enum TVPlayerFocusTarget: Hashable {
struct TVPlayerView: View { struct TVPlayerView: View {
@Environment(\.appEnvironment) private var appEnvironment @Environment(\.appEnvironment) private var appEnvironment
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var scenePhase
// MARK: - State // MARK: - State
@@ -397,6 +398,13 @@ struct TVPlayerView: View {
showControls() showControls()
} }
} }
// When app returns to foreground (e.g. after auto-pause from background),
// surface the controls so the user can immediately resume or navigate.
.onChange(of: scenePhase) { oldPhase, newPhase in
if newPhase == .active && oldPhase != .active {
showControls()
}
}
} }
// MARK: - Failure Overlays // MARK: - Failure Overlays

View File

@@ -271,12 +271,10 @@ private struct BehaviorSection: View {
} }
} }
#if os(iOS) || os(macOS)
Toggle( Toggle(
String(localized: "settings.playback.backgroundPlayback"), String(localized: "settings.playback.backgroundPlayback"),
isOn: $settings.backgroundPlaybackEnabled isOn: $settings.backgroundPlaybackEnabled
) )
#endif
#if os(tvOS) #if os(tvOS)
Toggle( Toggle(