Fix mid-stream audio loudness jumps

Root cause: the quality upgrade path called replaceCurrentItem mid-stream,
which re-loaded the HLS master manifest and re-picked an audio rendition,
producing a perceived loudness jump 10-30s into playback. .moviePlayback
mode amplified this by re-initializing cinematic audio processing on each
variant change.

- Start streams directly at user's desiredResolution; remove
  scheduleQualityUpgrade, qualityUpgradeTask, and the 504p->best swap.
- Switch AVAudioSession mode from .moviePlayback to .default in both
  MultiStreamView and SingleStreamPlayerView.
- Pin the HLS audio rendition by selecting the default audible
  MediaSelectionGroup option on every new AVPlayerItem, preventing
  ABR from swapping channel layouts mid-stream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-14 20:04:39 -05:00
parent ba24c767a0
commit 85a19fdd71
2 changed files with 54 additions and 102 deletions

View File

@@ -57,24 +57,37 @@ private func singleStreamTimeControlDescription(_ status: AVPlayer.TimeControlSt
}
private func makeSingleStreamPlayerItem(from source: SingleStreamPlaybackSource) -> AVPlayerItem {
let item: AVPlayerItem
if source.httpHeaders.isEmpty {
let item = AVPlayerItem(url: source.url)
item.preferredForwardBufferDuration = 8
return item
item = AVPlayerItem(url: source.url)
} else {
let assetOptions: [String: Any] = [
"AVURLAssetHTTPHeaderFieldsKey": source.httpHeaders,
]
let asset = AVURLAsset(url: source.url, options: assetOptions)
item = AVPlayerItem(asset: asset)
logSingleStream(
"Configured authenticated AVURLAsset headerKeys=\(singleStreamHeaderKeysDescription(source.httpHeaders))"
)
}
let assetOptions: [String: Any] = [
"AVURLAssetHTTPHeaderFieldsKey": source.httpHeaders,
]
let asset = AVURLAsset(url: source.url, options: assetOptions)
let item = AVPlayerItem(asset: asset)
item.preferredForwardBufferDuration = 8
logSingleStream(
"Configured authenticated AVURLAsset headerKeys=\(singleStreamHeaderKeysDescription(source.httpHeaders))"
)
pinSingleStreamAudioSelection(on: item)
return item
}
/// Lock the HLS audio rendition to the default option so ABR can't swap
/// to a different channel layout / loudness mid-stream.
private func pinSingleStreamAudioSelection(on item: AVPlayerItem) {
let asset = item.asset
Task { @MainActor [weak item] in
guard let group = try? await asset.loadMediaSelectionGroup(for: .audible),
let option = group.defaultOption ?? group.options.first,
let item else { return }
item.select(option, in: group)
logSingleStream("pinAudioSelection selected=\(option.displayName) options=\(group.options.count)")
}
}
struct SingleStreamPlaybackScreen: View {
@Environment(\.dismiss) private var dismiss
let resolveSource: @Sendable () async -> SingleStreamPlaybackSource?
@@ -544,9 +557,9 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
)
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
logSingleStream("AVAudioSession configured for playback")
logSingleStream("AVAudioSession configured for playback mode=default")
} catch {
logSingleStream("AVAudioSession configuration failed error=\(error.localizedDescription)")
}