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

@@ -323,14 +323,22 @@ extension QualitySelectorView {
@ViewBuilder
var generalSectionContent: some View {
#if os(tvOS)
// tvOS only has the speed row here; style it to match the Settings rows.
playbackSpeedRow
// tvOS rows are styled individually to match the Settings rows.
VStack(spacing: 8) {
playbackSpeedRow
audioModeRow
}
#else
VStack(spacing: 0) {
playbackSpeedRow
Divider()
audioModeRow
Divider()
lockControlsRow
}
.cardBackground()
@@ -408,6 +416,45 @@ extension QualitySelectorView {
#endif
}
@ViewBuilder
private var audioModeRow: some View {
#if os(tvOS)
Button {
onAudioModeToggled?(!isAudioMode)
} label: {
HStack {
Label(String(localized: "player.quality.audioMode"), systemImage: "music.note")
.font(.headline)
Spacer()
Text(isAudioMode ? String(localized: "common.on") : String(localized: "common.off"))
.foregroundStyle(.secondary)
}
.padding(.vertical, 14)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(TVSettingsRowButtonStyle())
#else
HStack {
Label(String(localized: "player.quality.audioMode"), systemImage: "music.note")
.font(.headline)
Spacer()
Toggle("", isOn: Binding(
get: { isAudioMode },
set: { onAudioModeToggled?($0) }
))
.labelsHidden()
}
.padding(.vertical, 12)
.padding(.horizontal, 12)
#endif
}
#if !os(tvOS)
@ViewBuilder
private var lockControlsRow: some View {
@@ -751,7 +798,11 @@ extension QualitySelectorView {
private func handleAudioStreamTap(_ stream: Stream) {
selectedAudioStream = stream
if let video = selectedVideoStream, video.isVideoOnly {
if currentStream?.isAudioOnly == true {
// Audio mode / audio-only content: the tapped track IS the main stream
onStreamSelected(stream, nil)
performDismiss()
} else if let video = selectedVideoStream, video.isVideoOnly {
onStreamSelected(video, stream)
performDismiss()
}