Compare commits
2 Commits
88308b46f5
...
ba24c767a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba24c767a0 | ||
|
|
bf44a7b7eb |
@@ -53,6 +53,8 @@ final class GamesViewModel {
|
|||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private var refreshTask: Task<Void, Never>?
|
private var refreshTask: Task<Void, Never>?
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
|
private var lastLoadDateString: String?
|
||||||
|
@ObservationIgnored
|
||||||
private var authenticatedVideoFeedCache: [String: AuthenticatedVideoFeedCacheEntry] = [:]
|
private var authenticatedVideoFeedCache: [String: AuthenticatedVideoFeedCacheEntry] = [:]
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private var videoShuffleBagsByModel: [String: [String: [URL]]] = [:]
|
private var videoShuffleBagsByModel: [String: [String: [URL]]] = [:]
|
||||||
@@ -105,12 +107,21 @@ final class GamesViewModel {
|
|||||||
func startAutoRefresh() {
|
func startAutoRefresh() {
|
||||||
stopAutoRefresh()
|
stopAutoRefresh()
|
||||||
refreshTask = Task { [weak self] in
|
refreshTask = Task { [weak self] in
|
||||||
|
var ticksSinceFullLoad = 0
|
||||||
while !Task.isCancelled {
|
while !Task.isCancelled {
|
||||||
try? await Task.sleep(for: .seconds(60))
|
try? await Task.sleep(for: .seconds(60))
|
||||||
guard !Task.isCancelled else { break }
|
guard !Task.isCancelled else { break }
|
||||||
guard let self else { break }
|
guard let self else { break }
|
||||||
// Refresh if there are live games or active streams
|
ticksSinceFullLoad += 1
|
||||||
if !self.liveGames.isEmpty || !self.activeStreams.isEmpty {
|
|
||||||
|
// Full schedule reload every 10 minutes (or immediately on day change)
|
||||||
|
let today = Self.dateFormatter.string(from: Date())
|
||||||
|
let dayChanged = self.lastLoadDateString != nil && self.lastLoadDateString != today
|
||||||
|
if dayChanged || ticksSinceFullLoad >= 10 {
|
||||||
|
ticksSinceFullLoad = 0
|
||||||
|
await self.loadGames()
|
||||||
|
} else if !self.liveGames.isEmpty || !self.activeStreams.isEmpty {
|
||||||
|
// Fast score refresh every 60s when games are live
|
||||||
await self.refreshScores()
|
await self.refreshScores()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +133,14 @@ final class GamesViewModel {
|
|||||||
refreshTask = nil
|
refreshTask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshIfDayChanged() async {
|
||||||
|
let today = Self.dateFormatter.string(from: Date())
|
||||||
|
if lastLoadDateString != today {
|
||||||
|
logGamesViewModel("Day changed (\(lastLoadDateString ?? "nil") → \(today)), reloading games")
|
||||||
|
await loadGames()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func refreshScores() async {
|
private func refreshScores() async {
|
||||||
let statsGames = await fetchStatsGames()
|
let statsGames = await fetchStatsGames()
|
||||||
guard !statsGames.isEmpty else { return }
|
guard !statsGames.isEmpty else { return }
|
||||||
@@ -227,6 +246,7 @@ final class GamesViewModel {
|
|||||||
errorMessage = "No games found"
|
errorMessage = "No games found"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastLoadDateString = todayDateString
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -260,7 +260,11 @@ struct DashboardView: View {
|
|||||||
mediaId: selection.broadcast.mediaId,
|
mediaId: selection.broadcast.mediaId,
|
||||||
streamURLString: selection.broadcast.streamURL
|
streamURLString: selection.broadcast.streamURL
|
||||||
)
|
)
|
||||||
guard let url = await viewModel.resolveStreamURL(for: stream) else { return nil }
|
guard let url = await viewModel.resolveStreamURL(
|
||||||
|
for: stream,
|
||||||
|
resolutionOverride: viewModel.defaultResolution,
|
||||||
|
preserveServerResolutionWhenBest: false
|
||||||
|
) else { return nil }
|
||||||
return SingleStreamPlaybackSource(url: url)
|
return SingleStreamPlaybackSource(url: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ struct GameCenterView: View {
|
|||||||
VideoPlayer(player: player)
|
VideoPlayer(player: player)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onAppear { player.play() }
|
.onAppear { player.play() }
|
||||||
|
.onDisappear { player.pause() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ private struct MultiStreamTile: View {
|
|||||||
@State private var hasError = false
|
@State private var hasError = false
|
||||||
@State private var startupPlaybackTask: Task<Void, Never>?
|
@State private var startupPlaybackTask: Task<Void, Never>?
|
||||||
@State private var qualityUpgradeTask: Task<Void, Never>?
|
@State private var qualityUpgradeTask: Task<Void, Never>?
|
||||||
|
@State private var werkoutMonitorTask: Task<Void, Never>?
|
||||||
@State private var clipTimeLimitObserver: Any?
|
@State private var clipTimeLimitObserver: Any?
|
||||||
@State private var isAdvancingClip = false
|
@State private var isAdvancingClip = false
|
||||||
@StateObject private var playbackDiagnostics = MultiStreamPlaybackDiagnostics()
|
@StateObject private var playbackDiagnostics = MultiStreamPlaybackDiagnostics()
|
||||||
@@ -447,7 +448,13 @@ private struct MultiStreamTile: View {
|
|||||||
startupPlaybackTask = nil
|
startupPlaybackTask = nil
|
||||||
qualityUpgradeTask?.cancel()
|
qualityUpgradeTask?.cancel()
|
||||||
qualityUpgradeTask = nil
|
qualityUpgradeTask = nil
|
||||||
if let player { removeClipTimeLimit(from: player) }
|
werkoutMonitorTask?.cancel()
|
||||||
|
werkoutMonitorTask = nil
|
||||||
|
if let player {
|
||||||
|
removeClipTimeLimit(from: player)
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
player = nil
|
||||||
playbackDiagnostics.clear(streamID: stream.id, reason: "tile disappeared")
|
playbackDiagnostics.clear(streamID: stream.id, reason: "tile disappeared")
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -557,7 +564,7 @@ private struct MultiStreamTile: View {
|
|||||||
|
|
||||||
if !Self.audioSessionConfigured {
|
if !Self.audioSessionConfigured {
|
||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback)
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
Self.audioSessionConfigured = true
|
Self.audioSessionConfigured = true
|
||||||
logMultiView("startStream audio session configured id=\(stream.id)")
|
logMultiView("startStream audio session configured id=\(stream.id)")
|
||||||
@@ -604,13 +611,14 @@ private struct MultiStreamTile: View {
|
|||||||
avPlayer.currentItem?.preferredForwardBufferDuration = 2
|
avPlayer.currentItem?.preferredForwardBufferDuration = 2
|
||||||
|
|
||||||
self.player = avPlayer
|
self.player = avPlayer
|
||||||
|
// Set mute state BEFORE playback to prevent audio spikes
|
||||||
|
viewModel.attachPlayer(avPlayer, to: stream.id)
|
||||||
playbackDiagnostics.attach(
|
playbackDiagnostics.attach(
|
||||||
to: avPlayer,
|
to: avPlayer,
|
||||||
streamID: stream.id,
|
streamID: stream.id,
|
||||||
label: stream.label,
|
label: stream.label,
|
||||||
onPlaybackEnded: playbackEndedHandler(for: avPlayer)
|
onPlaybackEnded: playbackEndedHandler(for: avPlayer)
|
||||||
)
|
)
|
||||||
viewModel.attachPlayer(avPlayer, to: stream.id)
|
|
||||||
scheduleStartupPlaybackRecovery(for: avPlayer)
|
scheduleStartupPlaybackRecovery(for: avPlayer)
|
||||||
scheduleQualityUpgrade(for: avPlayer)
|
scheduleQualityUpgrade(for: avPlayer)
|
||||||
logMultiView("startStream attached player id=\(stream.id) muted=\(avPlayer.isMuted) startupResolution=\(multiViewStartupResolution) fastStart=true calling playImmediately(atRate: 1.0)")
|
logMultiView("startStream attached player id=\(stream.id) muted=\(avPlayer.isMuted) startupResolution=\(multiViewStartupResolution) fastStart=true calling playImmediately(atRate: 1.0)")
|
||||||
@@ -694,7 +702,7 @@ private struct MultiStreamTile: View {
|
|||||||
let streamID = stream.id
|
let streamID = stream.id
|
||||||
let label = stream.label
|
let label = stream.label
|
||||||
qualityUpgradeTask = Task { @MainActor in
|
qualityUpgradeTask = Task { @MainActor in
|
||||||
let checkDelays: [Double] = [2.0, 4.0, 7.0]
|
let checkDelays: [Double] = [2.0, 4.0, 7.0, 15.0, 30.0]
|
||||||
|
|
||||||
for delay in checkDelays {
|
for delay in checkDelays {
|
||||||
try? await Task.sleep(for: .seconds(delay))
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
@@ -707,20 +715,15 @@ private struct MultiStreamTile: View {
|
|||||||
let itemStatus = multiViewItemStatusDescription(player.currentItem?.status ?? .unknown)
|
let itemStatus = multiViewItemStatusDescription(player.currentItem?.status ?? .unknown)
|
||||||
let likelyToKeepUp = player.currentItem?.isPlaybackLikelyToKeepUp ?? false
|
let likelyToKeepUp = player.currentItem?.isPlaybackLikelyToKeepUp ?? false
|
||||||
let bufferEmpty = player.currentItem?.isPlaybackBufferEmpty ?? false
|
let bufferEmpty = player.currentItem?.isPlaybackBufferEmpty ?? false
|
||||||
let currentResolution = currentStreamResolution(for: player) ?? "unknown"
|
let indicatedBitrate = player.currentItem?.accessLog()?.events.last?.indicatedBitrate ?? 0
|
||||||
let stable = (itemStatus == "readyToPlay" || likelyToKeepUp) && !bufferEmpty
|
let stable = (itemStatus == "readyToPlay" || likelyToKeepUp) && !bufferEmpty
|
||||||
|
|
||||||
logMultiView(
|
logMultiView(
|
||||||
"qualityUpgrade check id=\(streamID) delay=\(delay)s currentResolution=\(currentResolution) targetResolution=\(targetResolution) stable=\(stable) rate=\(player.rate)"
|
"qualityUpgrade check id=\(streamID) delay=\(delay)s targetResolution=\(targetResolution) stable=\(stable) rate=\(player.rate) indicatedBitrate=\(Int(indicatedBitrate))"
|
||||||
)
|
)
|
||||||
|
|
||||||
guard stable else { continue }
|
guard stable else { continue }
|
||||||
|
|
||||||
if currentResolution == targetResolution {
|
|
||||||
logMultiView("qualityUpgrade skip id=\(streamID) reason=already-\(targetResolution)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let upgradedURL = await viewModel.resolveStreamURL(
|
guard let upgradedURL = await viewModel.resolveStreamURL(
|
||||||
for: stream,
|
for: stream,
|
||||||
resolutionOverride: targetResolution,
|
resolutionOverride: targetResolution,
|
||||||
@@ -756,13 +759,6 @@ private struct MultiStreamTile: View {
|
|||||||
(player.currentItem?.asset as? AVURLAsset)?.url
|
(player.currentItem?.asset as? AVURLAsset)?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
private func currentStreamResolution(for player: AVPlayer) -> String? {
|
|
||||||
guard let url = currentStreamURL(for: player) else { return nil }
|
|
||||||
return URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
|
||||||
.queryItems?
|
|
||||||
.first(where: { $0.name == "resolution" })?
|
|
||||||
.value
|
|
||||||
}
|
|
||||||
|
|
||||||
private func installClipTimeLimit(on player: AVPlayer) {
|
private func installClipTimeLimit(on player: AVPlayer) {
|
||||||
removeClipTimeLimit(from: player)
|
removeClipTimeLimit(from: player)
|
||||||
@@ -851,9 +847,11 @@ private struct MultiStreamTile: View {
|
|||||||
installClipTimeLimit(on: player)
|
installClipTimeLimit(on: player)
|
||||||
|
|
||||||
// Monitor for failure and auto-skip to next clip
|
// Monitor for failure and auto-skip to next clip
|
||||||
Task { @MainActor in
|
werkoutMonitorTask?.cancel()
|
||||||
|
werkoutMonitorTask = Task { @MainActor in
|
||||||
for checkDelay in [1.0, 3.0] {
|
for checkDelay in [1.0, 3.0] {
|
||||||
try? await Task.sleep(for: .seconds(checkDelay))
|
try? await Task.sleep(for: .seconds(checkDelay))
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
let postItemStatus = player.currentItem?.status
|
let postItemStatus = player.currentItem?.status
|
||||||
let error = player.currentItem?.error?.localizedDescription ?? "nil"
|
let error = player.currentItem?.error?.localizedDescription ?? "nil"
|
||||||
logMultiView(
|
logMultiView(
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ private func singleStreamTimeControlDescription(_ status: AVPlayer.TimeControlSt
|
|||||||
private func makeSingleStreamPlayerItem(from source: SingleStreamPlaybackSource) -> AVPlayerItem {
|
private func makeSingleStreamPlayerItem(from source: SingleStreamPlaybackSource) -> AVPlayerItem {
|
||||||
if source.httpHeaders.isEmpty {
|
if source.httpHeaders.isEmpty {
|
||||||
let item = AVPlayerItem(url: source.url)
|
let item = AVPlayerItem(url: source.url)
|
||||||
item.preferredForwardBufferDuration = 2
|
item.preferredForwardBufferDuration = 8
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ private func makeSingleStreamPlayerItem(from source: SingleStreamPlaybackSource)
|
|||||||
]
|
]
|
||||||
let asset = AVURLAsset(url: source.url, options: assetOptions)
|
let asset = AVURLAsset(url: source.url, options: assetOptions)
|
||||||
let item = AVPlayerItem(asset: asset)
|
let item = AVPlayerItem(asset: asset)
|
||||||
item.preferredForwardBufferDuration = 2
|
item.preferredForwardBufferDuration = 8
|
||||||
logSingleStream(
|
logSingleStream(
|
||||||
"Configured authenticated AVURLAsset headerKeys=\(singleStreamHeaderKeysDescription(source.httpHeaders))"
|
"Configured authenticated AVURLAsset headerKeys=\(singleStreamHeaderKeysDescription(source.httpHeaders))"
|
||||||
)
|
)
|
||||||
@@ -553,15 +553,16 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
let playerItem = makeSingleStreamPlayerItem(from: source)
|
let playerItem = makeSingleStreamPlayerItem(from: source)
|
||||||
let player = AVPlayer(playerItem: playerItem)
|
let player = AVPlayer(playerItem: playerItem)
|
||||||
player.automaticallyWaitsToMinimizeStalling = false
|
player.automaticallyWaitsToMinimizeStalling = true
|
||||||
player.isMuted = source.forceMuteAudio
|
player.isMuted = source.forceMuteAudio
|
||||||
logSingleStream("Configured player for fast start preferredForwardBufferDuration=2 automaticallyWaitsToMinimizeStalling=false")
|
logSingleStream("Configured player for quality ramp preferredForwardBufferDuration=8 automaticallyWaitsToMinimizeStalling=true")
|
||||||
context.coordinator.attachDebugObservers(to: player, url: url, resolveNextSource: resolveNextSource)
|
context.coordinator.attachDebugObservers(to: player, url: url, resolveNextSource: resolveNextSource)
|
||||||
controller.player = player
|
controller.player = player
|
||||||
logSingleStream("AVPlayer assigned to controller; calling playImmediately(atRate: 1.0)")
|
logSingleStream("AVPlayer assigned to controller; calling playImmediately(atRate: 1.0)")
|
||||||
player.playImmediately(atRate: 1.0)
|
player.playImmediately(atRate: 1.0)
|
||||||
context.coordinator.installClipTimeLimit(on: player, resolveNextSource: resolveNextSource)
|
context.coordinator.installClipTimeLimit(on: player, resolveNextSource: resolveNextSource)
|
||||||
context.coordinator.scheduleStartupRecovery(for: player)
|
context.coordinator.scheduleStartupRecovery(for: player)
|
||||||
|
context.coordinator.scheduleQualityMonitor(for: player)
|
||||||
}
|
}
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
@@ -583,11 +584,11 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: Coordinator) {
|
static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: Coordinator) {
|
||||||
logSingleStream("dismantleUIViewController start isPiPActive=\(coordinator.isPiPActive)")
|
logSingleStream("dismantleUIViewController start isPiPActive=\(coordinator.isPiPActive)")
|
||||||
|
coordinator.clearDebugObservers()
|
||||||
if coordinator.isPiPActive {
|
if coordinator.isPiPActive {
|
||||||
logSingleStream("dismantleUIViewController skipped — PiP is active")
|
logSingleStream("dismantleUIViewController — PiP active, observers cleared but keeping player")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
coordinator.clearDebugObservers()
|
|
||||||
uiViewController.player?.pause()
|
uiViewController.player?.pause()
|
||||||
uiViewController.player = nil
|
uiViewController.player = nil
|
||||||
logSingleStream("dismantleUIViewController complete")
|
logSingleStream("dismantleUIViewController complete")
|
||||||
@@ -597,6 +598,7 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
|
|||||||
private var playerObservations: [NSKeyValueObservation] = []
|
private var playerObservations: [NSKeyValueObservation] = []
|
||||||
private var notificationTokens: [NSObjectProtocol] = []
|
private var notificationTokens: [NSObjectProtocol] = []
|
||||||
private var startupRecoveryTask: Task<Void, Never>?
|
private var startupRecoveryTask: Task<Void, Never>?
|
||||||
|
private var qualityMonitorTask: Task<Void, Never>?
|
||||||
private var clipTimeLimitObserver: Any?
|
private var clipTimeLimitObserver: Any?
|
||||||
private static let maxClipDuration: Double = 15.0
|
private static let maxClipDuration: Double = 15.0
|
||||||
var onTogglePitchInfo: (() -> Void)?
|
var onTogglePitchInfo: (() -> Void)?
|
||||||
@@ -863,9 +865,38 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scheduleQualityMonitor(for player: AVPlayer) {
|
||||||
|
qualityMonitorTask?.cancel()
|
||||||
|
qualityMonitorTask = Task { @MainActor [weak player] in
|
||||||
|
// Check at 5s, 15s, and 30s whether AVPlayer has ramped to a reasonable bitrate
|
||||||
|
for delay in [5.0, 15.0, 30.0] {
|
||||||
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
|
guard !Task.isCancelled, let player else { return }
|
||||||
|
|
||||||
|
let indicatedBitrate = player.currentItem?.accessLog()?.events.last?.indicatedBitrate ?? 0
|
||||||
|
let observedBitrate = player.currentItem?.accessLog()?.events.last?.observedBitrate ?? 0
|
||||||
|
let likelyToKeepUp = player.currentItem?.isPlaybackLikelyToKeepUp ?? false
|
||||||
|
|
||||||
|
logSingleStream(
|
||||||
|
"qualityMonitor check delay=\(delay)s indicatedBitrate=\(Int(indicatedBitrate)) observedBitrate=\(Int(observedBitrate)) likelyToKeepUp=\(likelyToKeepUp) rate=\(player.rate)"
|
||||||
|
)
|
||||||
|
|
||||||
|
// If observed bandwidth supports higher quality but indicated is low, nudge AVPlayer
|
||||||
|
if likelyToKeepUp && indicatedBitrate > 0 && observedBitrate > indicatedBitrate * 2 {
|
||||||
|
logSingleStream(
|
||||||
|
"qualityMonitor nudge delay=\(delay)s — observed bandwidth \(Int(observedBitrate)) >> indicated \(Int(indicatedBitrate)), setting preferredPeakBitRate=0 to uncap"
|
||||||
|
)
|
||||||
|
player.currentItem?.preferredPeakBitRate = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func clearDebugObservers() {
|
func clearDebugObservers() {
|
||||||
startupRecoveryTask?.cancel()
|
startupRecoveryTask?.cancel()
|
||||||
startupRecoveryTask = nil
|
startupRecoveryTask = nil
|
||||||
|
qualityMonitorTask?.cancel()
|
||||||
|
qualityMonitorTask = nil
|
||||||
playerObservations.removeAll()
|
playerObservations.removeAll()
|
||||||
for token in notificationTokens {
|
for token in notificationTokens {
|
||||||
NotificationCenter.default.removeObserver(token)
|
NotificationCenter.default.removeObserver(token)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SwiftUI
|
|||||||
@main
|
@main
|
||||||
struct mlbTVOSApp: App {
|
struct mlbTVOSApp: App {
|
||||||
@State private var viewModel = GamesViewModel()
|
@State private var viewModel = GamesViewModel()
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
configureAudioSession()
|
configureAudioSession()
|
||||||
@@ -13,6 +14,11 @@ struct mlbTVOSApp: App {
|
|||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
.environment(viewModel)
|
.environment(viewModel)
|
||||||
|
.onChange(of: scenePhase) { _, newPhase in
|
||||||
|
if newPhase == .active {
|
||||||
|
Task { await viewModel.refreshIfDayChanged() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,5 +30,23 @@ struct mlbTVOSApp: App {
|
|||||||
} catch {
|
} catch {
|
||||||
print("Failed to set audio session: \(error)")
|
print("Failed to set audio session: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: AVAudioSession.interruptionNotification,
|
||||||
|
object: AVAudioSession.sharedInstance(),
|
||||||
|
queue: .main
|
||||||
|
) { notification in
|
||||||
|
guard let info = notification.userInfo,
|
||||||
|
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||||
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
|
||||||
|
|
||||||
|
if type == .ended {
|
||||||
|
let options = (info[AVAudioSessionInterruptionOptionKey] as? UInt)
|
||||||
|
.flatMap(AVAudioSession.InterruptionOptions.init) ?? []
|
||||||
|
if options.contains(.shouldResume) {
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user