Add AudioDiagnostics heartbeat logging

Adds [AUDIO]-prefixed logs to both single-stream and multi-stream players:
1 Hz heartbeat with rate/timeControl/mute/volume/bitrate/route, plus
immediate events on rate, isMuted, volume, currentItem, media selection,
access-log, error-log, and system audio route/interruption changes.

Grep Xcode console for `[AUDIO]` or `[AUDIO SYSTEM]` to isolate.

Also reverts the AAC-preference in pinAudioSelection: the
ballgame.treytartt.com master playlist is already all mp4a.40.2 stereo,
so the Dolby-DRC theory doesn't fit. Pin simply selects the default
audible option now.

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

View File

@@ -75,8 +75,7 @@ private func makeSingleStreamPlayerItem(from source: SingleStreamPlaybackSource)
return item
}
/// Lock the HLS audio rendition to the default option so ABR can't swap
/// to a different channel layout / loudness mid-stream.
/// Pin the HLS audio rendition so ABR can't swap channel layouts mid-stream.
private func pinSingleStreamAudioSelection(on item: AVPlayerItem) {
let asset = item.asset
Task { @MainActor [weak item] in
@@ -571,6 +570,10 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
logSingleStream("Configured player for quality ramp preferredForwardBufferDuration=8 automaticallyWaitsToMinimizeStalling=true")
context.coordinator.attachDebugObservers(to: player, url: url, resolveNextSource: resolveNextSource)
controller.player = player
if context.coordinator.audioDiagnostics == nil {
context.coordinator.audioDiagnostics = AudioDiagnostics(tag: "single")
}
context.coordinator.audioDiagnostics?.attach(to: player)
logSingleStream("AVPlayer assigned to controller; calling playImmediately(atRate: 1.0)")
player.playImmediately(atRate: 1.0)
context.coordinator.installClipTimeLimit(on: player, resolveNextSource: resolveNextSource)
@@ -602,6 +605,10 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
logSingleStream("dismantleUIViewController — PiP active, observers cleared but keeping player")
return
}
Task { @MainActor in
coordinator.audioDiagnostics?.detach()
coordinator.audioDiagnostics = nil
}
uiViewController.player?.pause()
uiViewController.player = nil
logSingleStream("dismantleUIViewController complete")
@@ -613,6 +620,7 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
private var startupRecoveryTask: Task<Void, Never>?
private var qualityMonitorTask: Task<Void, Never>?
private var clipTimeLimitObserver: Any?
var audioDiagnostics: AudioDiagnostics?
private static let maxClipDuration: Double = 15.0
var onTogglePitchInfo: (() -> Void)?
var onToggleGameCenter: (() -> Void)?