Add Werkout channel playback and autoplay

This commit is contained in:
Trey t
2026-03-26 20:53:08 -05:00
parent bae265b132
commit 127125ae1b
6 changed files with 957 additions and 48 deletions

View File

@@ -41,6 +41,11 @@ private func multiViewTimeControlDescription(_ status: AVPlayer.TimeControlStatu
}
}
private func multiViewHeaderKeysDescription(_ headers: [String: String]) -> String {
guard !headers.isEmpty else { return "none" }
return headers.keys.sorted().joined(separator: ",")
}
struct MultiStreamView: View {
@Environment(GamesViewModel.self) private var viewModel
@State private var selectedStream: StreamSelection?
@@ -497,11 +502,18 @@ private struct MultiStreamTile: View {
}
private func startStream() async {
logMultiView("startStream begin id=\(stream.id) label=\(stream.label) hasInlinePlayer=\(player != nil) hasSharedPlayer=\(stream.player != nil) hasOverrideURL=\(stream.overrideURL != nil)")
logMultiView(
"startStream begin id=\(stream.id) label=\(stream.label) hasInlinePlayer=\(player != nil) hasSharedPlayer=\(stream.player != nil) hasOverrideURL=\(stream.overrideURL != nil) hasOverrideHeaders=\(stream.overrideHeaders != nil)"
)
if let player {
player.isMuted = viewModel.audioFocusStreamID != stream.id
playbackDiagnostics.attach(to: player, streamID: stream.id, label: stream.label)
playbackDiagnostics.attach(
to: player,
streamID: stream.id,
label: stream.label,
onPlaybackEnded: playbackEndedHandler(for: player)
)
scheduleStartupPlaybackRecovery(for: player)
scheduleQualityUpgrade(for: player)
logMultiView("startStream reused inline player id=\(stream.id) muted=\(player.isMuted)")
@@ -520,7 +532,12 @@ private struct MultiStreamTile: View {
existingPlayer.isMuted = viewModel.audioFocusStreamID != stream.id
self.player = existingPlayer
hasError = false
playbackDiagnostics.attach(to: existingPlayer, streamID: stream.id, label: stream.label)
playbackDiagnostics.attach(
to: existingPlayer,
streamID: stream.id,
label: stream.label,
onPlaybackEnded: playbackEndedHandler(for: existingPlayer)
)
scheduleStartupPlaybackRecovery(for: existingPlayer)
scheduleQualityUpgrade(for: existingPlayer)
logMultiView("startStream reused shared player id=\(stream.id) muted=\(existingPlayer.isMuted)")
@@ -544,13 +561,17 @@ private struct MultiStreamTile: View {
return
}
logMultiView("startStream creating AVPlayer id=\(stream.id) url=\(url.absoluteString)")
let avPlayer = AVPlayer(url: url)
let avPlayer = makePlayer(url: url, headers: stream.overrideHeaders)
avPlayer.automaticallyWaitsToMinimizeStalling = false
avPlayer.currentItem?.preferredForwardBufferDuration = 2
self.player = avPlayer
playbackDiagnostics.attach(to: avPlayer, streamID: stream.id, label: stream.label)
playbackDiagnostics.attach(
to: avPlayer,
streamID: stream.id,
label: stream.label,
onPlaybackEnded: playbackEndedHandler(for: avPlayer)
)
viewModel.attachPlayer(avPlayer, to: stream.id)
scheduleStartupPlaybackRecovery(for: avPlayer)
scheduleQualityUpgrade(for: avPlayer)
@@ -558,6 +579,28 @@ private struct MultiStreamTile: View {
avPlayer.playImmediately(atRate: 1.0)
}
private func makePlayer(url: URL, headers: [String: String]?) -> AVPlayer {
let headers = headers ?? [:]
logMultiView(
"startStream creating AVPlayer id=\(stream.id) url=\(url.absoluteString) headerKeys=\(multiViewHeaderKeysDescription(headers))"
)
let item = makePlayerItem(url: url, headers: headers)
return AVPlayer(playerItem: item)
}
private func makePlayerItem(url: URL, headers: [String: String]) -> AVPlayerItem {
if headers.isEmpty {
return AVPlayerItem(url: url)
}
let assetOptions: [String: Any] = [
"AVURLAssetHTTPHeaderFieldsKey": headers,
]
let asset = AVURLAsset(url: url, options: assetOptions)
return AVPlayerItem(asset: asset)
}
private func scheduleStartupPlaybackRecovery(for player: AVPlayer) {
startupPlaybackTask?.cancel()
@@ -681,6 +724,49 @@ private struct MultiStreamTile: View {
.first(where: { $0.name == "resolution" })?
.value
}
private func playbackEndedHandler(for player: AVPlayer) -> (() -> Void)? {
guard stream.id == SpecialPlaybackChannelConfig.werkoutNSFWStreamID else { return nil }
return {
Task { @MainActor in
await playNextWerkoutClip(on: player)
}
}
}
private func playNextWerkoutClip(on player: AVPlayer) async {
let currentURL = currentStreamURL(for: player)
logMultiView(
"playNextWerkoutClip begin id=\(stream.id) currentURL=\(currentURL?.absoluteString ?? "nil")"
)
guard let nextURL = await viewModel.resolveNextAuthenticatedFeedURLForActiveStream(
id: stream.id,
feedURL: SpecialPlaybackChannelConfig.werkoutNSFWFeedURL,
headers: SpecialPlaybackChannelConfig.werkoutNSFWHeaders
) else {
logMultiView("playNextWerkoutClip failed id=\(stream.id) reason=resolve-nil")
return
}
let nextItem = makePlayerItem(
url: nextURL,
headers: stream.overrideHeaders ?? SpecialPlaybackChannelConfig.werkoutNSFWHeaders
)
nextItem.preferredForwardBufferDuration = 2
player.replaceCurrentItem(with: nextItem)
player.automaticallyWaitsToMinimizeStalling = false
playbackDiagnostics.attach(
to: player,
streamID: stream.id,
label: stream.label,
onPlaybackEnded: playbackEndedHandler(for: player)
)
viewModel.attachPlayer(player, to: stream.id)
scheduleStartupPlaybackRecovery(for: player)
logMultiView("playNextWerkoutClip replay id=\(stream.id) url=\(nextURL.absoluteString)")
player.playImmediately(atRate: 1.0)
}
}
private struct MultiStreamPlayerLayerView: UIViewRepresentable {
@@ -742,7 +828,12 @@ private final class MultiStreamPlaybackDiagnostics: ObservableObject {
private var attachedPlayerIdentifier: ObjectIdentifier?
private var attachedItemIdentifier: ObjectIdentifier?
func attach(to player: AVPlayer, streamID: String, label: String) {
func attach(
to player: AVPlayer,
streamID: String,
label: String,
onPlaybackEnded: (() -> Void)? = nil
) {
let playerIdentifier = ObjectIdentifier(player)
let itemIdentifier = player.currentItem.map { ObjectIdentifier($0) }
if attachedPlayerIdentifier == playerIdentifier, attachedItemIdentifier == itemIdentifier {
@@ -824,6 +915,17 @@ private final class MultiStreamPlaybackDiagnostics: ObservableObject {
}
)
notificationTokens.append(
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: item,
queue: .main
) { _ in
logMultiView("playerItem didPlayToEnd id=\(streamID)")
onPlaybackEnded?()
}
)
notificationTokens.append(
NotificationCenter.default.addObserver(
forName: .AVPlayerItemNewErrorLogEntry,