Add audio-only music mode

Persistent per-platform mode that plays only the audio track to save
bandwidth and speed up loads. When enabled, stream selection picks the
best audio-only stream (preferred language, codec, bitrate) and never
fetches the video URL; toggling mid-video reloads at the current
position using the quality-switch path (live vid=no toggling is avoided
due to A/V desync).

- Toggle row in the player quality/settings sheet on iOS, macOS and
  tvOS, next to lock controls
- New audioMode control button type (red when active) for player
  controls and mini player layouts
- Picking a video quality explicitly turns the mode off; audio track
  picks keep it on and now work while audio-only is playing
- Stream URL refresh preserves audio-only playback instead of
  resurrecting video
- Queue preloads and history entries resolved before a toggle are
  discarded so selection re-runs with the current mode
- tvOS player shows the video thumbnail instead of a black screen
  during audio-only playback
- PiP button hidden for audio-only streams
This commit is contained in:
Arkadiusz Fal
2026-07-13 19:06:01 +02:00
parent 66f7602d08
commit 06087220ef
19 changed files with 352 additions and 85 deletions

View File

@@ -39,6 +39,11 @@ struct QualitySelectorView: View {
/// Callback when lock state changes
var onLockToggled: ((Bool) -> Void)?
/// Whether audio-only (music) mode is enabled
var isAudioMode: Bool = false
/// Callback when audio mode is toggled
var onAudioModeToggled: ((Bool) -> Void)?
/// Initial tab to show when view appears
var initialTab: QualitySelectorTab = .video
/// Whether to show the segmented tab picker (false for focused single-tab mode)
@@ -160,6 +165,7 @@ struct QualitySelectorView: View {
localCaptionURL: URL? = nil,
currentRate: PlaybackRate = .x1,
isControlsLocked: Bool = false,
isAudioMode: Bool = false,
initialTab: QualitySelectorTab = .video,
showTabPicker: Bool = true,
onStreamSelected: @escaping (Stream, Stream?) -> Void,
@@ -168,6 +174,7 @@ struct QualitySelectorView: View {
onSwitchToOnlineStream: @escaping (Stream, Stream?) -> Void = { _, _ in },
onRateChanged: ((PlaybackRate) -> Void)? = nil,
onLockToggled: ((Bool) -> Void)? = nil,
onAudioModeToggled: ((Bool) -> Void)? = nil,
onDismiss: (() -> Void)? = nil
) {
self.streams = streams
@@ -183,12 +190,14 @@ struct QualitySelectorView: View {
self.showTabPicker = showTabPicker
self.currentRate = currentRate
self.isControlsLocked = isControlsLocked
self.isAudioMode = isAudioMode
self.onStreamSelected = onStreamSelected
self.onCaptionSelected = onCaptionSelected
self.onLoadOnlineStreams = onLoadOnlineStreams
self.onSwitchToOnlineStream = onSwitchToOnlineStream
self.onRateChanged = onRateChanged
self.onLockToggled = onLockToggled
self.onAudioModeToggled = onAudioModeToggled
self.onDismiss = onDismiss
}
@@ -291,7 +300,10 @@ struct QualitySelectorView: View {
}
.onAppear {
selectedVideoStream = currentStream
selectedAudioStream = currentAudioStream ?? defaultAudioStream
// In audio mode the audio track IS the main stream
selectedAudioStream = currentStream?.isAudioOnly == true
? currentStream
: (currentAudioStream ?? defaultAudioStream)
#if os(tvOS)
// Defer until after the slide-in transition so the focus engine
// has finished routing focus away from the (now hidden) player