Fix NSFW sheet scroll on iOS/iPad, clean up audio pin

- WerkoutNSFWSheet: wrap content in ScrollView + ViewThatFits(in: .horizontal)
  so iPad's narrow sheet width falls back to VStack and content scrolls.
- Tighten padding on compact layouts (38→24).
- Revert AAC-preference in pinAudioSelection (stream is all AAC, no Dolby).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-16 19:30:25 -05:00
parent 08ad702f9d
commit da033cf12c
3 changed files with 122 additions and 28 deletions

View File

@@ -623,7 +623,8 @@ struct WerkoutNSFWSheet: View {
sheetBackground
.ignoresSafeArea()
ViewThatFits {
ScrollView(.vertical, showsIndicators: false) {
ViewThatFits(in: .horizontal) {
HStack(alignment: .top, spacing: 32) {
overviewColumn
.frame(maxWidth: .infinity, alignment: .leading)
@@ -638,7 +639,8 @@ struct WerkoutNSFWSheet: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(38)
}
.padding(usesStackedLayout ? 24 : 38)
.background(
RoundedRectangle(cornerRadius: 34, style: .continuous)
.fill(.black.opacity(0.46))

View File

@@ -630,7 +630,10 @@ private struct MultiStreamTile: View {
)
let item = makePlayerItem(url: url, headers: headers)
return AVPlayer(playerItem: item)
let player = AVPlayer(playerItem: item)
player.appliesMediaSelectionCriteriaAutomatically = false
logMultiView("startStream configured AVPlayer id=\(stream.id) appliesMediaSelectionCriteriaAutomatically=false")
return player
}
private func makePlayerItem(url: URL, headers: [String: String]) -> AVPlayerItem {
@@ -644,6 +647,8 @@ private struct MultiStreamTile: View {
let asset = AVURLAsset(url: url, options: assetOptions)
item = AVPlayerItem(asset: asset)
}
item.allowedAudioSpatializationFormats = []
logMultiView("startStream configured player item id=\(stream.id) allowedAudioSpatializationFormats=[]")
pinAudioSelection(on: item)
return item
}
@@ -651,13 +656,8 @@ private struct MultiStreamTile: View {
/// Pin the HLS audio rendition so ABR can't swap channel layouts mid-stream.
private func pinAudioSelection(on item: AVPlayerItem) {
let streamID = stream.id
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)
logMultiView("pinAudioSelection id=\(streamID) selected=\(option.displayName) options=\(group.options.count)")
Task { @MainActor in
await enforcePinnedMultiStreamAudioSelection(on: item, streamID: streamID)
}
}
@@ -818,6 +818,45 @@ private struct MultiStreamTile: View {
}
}
@MainActor
private func enforcePinnedMultiStreamAudioSelection(on item: AVPlayerItem, streamID: String) async {
let asset = item.asset
guard let group = try? await asset.loadMediaSelectionGroup(for: .audible),
let option = preferredMultiStreamAudioOption(in: group) else { return }
let current = item.currentMediaSelection.selectedMediaOption(in: group)
if current != option {
item.select(option, in: group)
}
logMultiView(
"pinAudioSelection id=\(streamID) selected=\(option.displayName) current=\(current?.displayName ?? "nil") options=\(group.options.count)"
)
}
private func preferredMultiStreamAudioOption(in group: AVMediaSelectionGroup) -> AVMediaSelectionOption? {
let defaultOption = group.defaultOption
return group.options.max { lhs, rhs in
multiStreamAudioPreferenceScore(for: lhs, defaultOption: defaultOption) < multiStreamAudioPreferenceScore(for: rhs, defaultOption: defaultOption)
} ?? defaultOption ?? group.options.first
}
private func multiStreamAudioPreferenceScore(for option: AVMediaSelectionOption, defaultOption: AVMediaSelectionOption?) -> Int {
let name = option.displayName.lowercased()
var score = 0
if option == defaultOption { score += 40 }
if name.contains("stereo") || name.contains("2.0") || name.contains("main") { score += 30 }
if name.contains("english") || name.contains("eng") { score += 20 }
if name.contains("surround") || name.contains("5.1") || name.contains("atmos") { score -= 30 }
if name.contains("spanish") || name.contains("sap") || name.contains("descriptive") || name.contains("alternate") {
score -= 25
}
if option.hasMediaCharacteristic(.describesVideoForAccessibility) {
score -= 40
}
return score
}
private struct MultiStreamPlayerLayerView: UIViewRepresentable {
let player: AVPlayer
let streamID: String
@@ -1548,6 +1587,7 @@ final class AudioDiagnostics {
) { [weak self, weak item] _ in
guard let self, let item else { return }
Task { @MainActor in
await enforcePinnedMultiStreamAudioSelection(on: item, streamID: self.tag)
let asset = item.asset
guard let group = try? await asset.loadMediaSelectionGroup(for: .audible),
let selected = item.currentMediaSelection.selectedMediaOption(in: group) else {

View File

@@ -71,22 +71,58 @@ private func makeSingleStreamPlayerItem(from source: SingleStreamPlaybackSource)
)
}
item.preferredForwardBufferDuration = 8
item.allowedAudioSpatializationFormats = []
logSingleStream("Configured player item preferredForwardBufferDuration=8 allowedAudioSpatializationFormats=[]")
pinSingleStreamAudioSelection(on: item)
return item
}
/// 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
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)")
Task { @MainActor in
await enforcePinnedSingleStreamAudioSelection(on: item)
}
}
@MainActor
private func enforcePinnedSingleStreamAudioSelection(on item: AVPlayerItem) async {
let asset = item.asset
guard let group = try? await asset.loadMediaSelectionGroup(for: .audible),
let option = preferredSingleStreamAudioOption(in: group) else { return }
let current = item.currentMediaSelection.selectedMediaOption(in: group)
if current != option {
item.select(option, in: group)
}
logSingleStream(
"pinAudioSelection selected=\(option.displayName) current=\(current?.displayName ?? "nil") options=\(group.options.count)"
)
}
private func preferredSingleStreamAudioOption(in group: AVMediaSelectionGroup) -> AVMediaSelectionOption? {
let defaultOption = group.defaultOption
return group.options.max { lhs, rhs in
audioPreferenceScore(for: lhs, defaultOption: defaultOption) < audioPreferenceScore(for: rhs, defaultOption: defaultOption)
} ?? defaultOption ?? group.options.first
}
private func audioPreferenceScore(for option: AVMediaSelectionOption, defaultOption: AVMediaSelectionOption?) -> Int {
let name = option.displayName.lowercased()
var score = 0
if option == defaultOption { score += 40 }
if name.contains("stereo") || name.contains("2.0") || name.contains("main") { score += 30 }
if name.contains("english") || name.contains("eng") { score += 20 }
if name.contains("surround") || name.contains("5.1") || name.contains("atmos") { score -= 30 }
if name.contains("spanish") || name.contains("sap") || name.contains("descriptive") || name.contains("alternate") {
score -= 25
}
if option.hasMediaCharacteristic(.describesVideoForAccessibility) {
score -= 40
}
return score
}
struct SingleStreamPlaybackScreen: View {
@Environment(\.dismiss) private var dismiss
let resolveSource: @Sendable () async -> SingleStreamPlaybackSource?
@@ -565,9 +601,12 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
let playerItem = makeSingleStreamPlayerItem(from: source)
let player = AVPlayer(playerItem: playerItem)
player.appliesMediaSelectionCriteriaAutomatically = false
player.automaticallyWaitsToMinimizeStalling = true
player.isMuted = source.forceMuteAudio
logSingleStream("Configured player for quality ramp preferredForwardBufferDuration=8 automaticallyWaitsToMinimizeStalling=true")
logSingleStream(
"Configured player for quality ramp preferredForwardBufferDuration=8 automaticallyWaitsToMinimizeStalling=true appliesMediaSelectionCriteriaAutomatically=false"
)
context.coordinator.attachDebugObservers(to: player, url: url, resolveNextSource: resolveNextSource)
controller.player = player
if context.coordinator.audioDiagnostics == nil {
@@ -812,6 +851,19 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
}
}
)
notificationTokens.append(
NotificationCenter.default.addObserver(
forName: AVPlayerItem.mediaSelectionDidChangeNotification,
object: item,
queue: .main
) { _ in
logSingleStream("Notification mediaSelectionDidChange")
Task { @MainActor in
await enforcePinnedSingleStreamAudioSelection(on: item)
}
}
)
}
func scheduleStartupRecovery(for player: AVPlayer) {