Add Werkout channel playback and autoplay
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user