From 44e0f0afbbbb819f8ff7d43be374b9a60bfbc7d7 Mon Sep 17 00:00:00 2001 From: tahakocal Date: Tue, 27 Jan 2026 12:30:20 +0300 Subject: [PATCH] fix: observe AVPlayerItem.status for HLS streaming auto-start When playing HLS streams, audio doesn't start on first play() call because AVPlayerItem.status is still .unknown at that point. AVPlayer ignores rate changes when item is not ready. This fix adds observation of AVPlayerItem.status. When status becomes .readyToPlay and playWhenReady is true, we call applyAVPlayerRate() to start playback. Problem: 1. play() is called -> playWhenReady = true 2. applyAVPlayerRate() sets rate, but item.status is .unknown 3. AVPlayer ignores the rate change 4. Item becomes .readyToPlay but nothing triggers playback 5. Audio doesn't play until pause/play cycle Solution: 1. Observe AVPlayerItem.status changes 2. When status becomes .readyToPlay and playWhenReady is true 3. Call applyAVPlayerRate() to start actual playback This is consistent with Apple's guidance from WWDC 2016: "Observe the status property of AVPlayerItem and only begin playback when status is .readyToPlay" Fixes HLS streams not auto-starting on iOS. --- .../AVPlayerWrapper/AVPlayerWrapper.swift | 14 +++++++++-- .../Observer/AVPlayerItemObserver.swift | 24 +++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift index a7b168e..74ad797 100755 --- a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift @@ -508,11 +508,21 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate { state = .ready } } - + + func item(didUpdateStatus status: AVPlayerItem.Status) { + // When the item becomes ready to play and playWhenReady is true, + // we need to apply the rate again to start playback. + // This is crucial for streaming content (HLS) where the item + // may not be ready immediately when play() is called. + if status == .readyToPlay && playWhenReady { + applyAVPlayerRate() + } + } + func item(didUpdateDuration duration: Double) { delegate?.AVWrapper(didUpdateDuration: duration) } - + func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) { delegate?.AVWrapper(didReceiveTimedMetadata: metadata) } diff --git a/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift b/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift index f46e3d3..1b033b4 100644 --- a/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift +++ b/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift @@ -9,21 +9,28 @@ import Foundation import AVFoundation protocol AVPlayerItemObserverDelegate: AnyObject { - + /** Called when the duration of the observed item is updated. */ func item(didUpdateDuration duration: Double) - + /** Called when the playback of the observed item is or is no longer likely to keep up. */ func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) + + /** + Called when the status of the observed item changes. + This is important for streaming content (HLS) where the item may not be ready immediately. + */ + func item(didUpdateStatus status: AVPlayerItem.Status) + /** Called when the observed item receives metadata */ func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) - + } /** @@ -38,6 +45,7 @@ class AVPlayerItemObserver: NSObject { static let duration = #keyPath(AVPlayerItem.duration) static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges) static let playbackLikelyToKeepUp = #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp) + static let status = #keyPath(AVPlayerItem.status) } private(set) var isObserving: Bool = false @@ -66,6 +74,7 @@ class AVPlayerItemObserver: NSObject { item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context) item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context) item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context) + item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.status, options: [.new], context: &AVPlayerItemObserver.context) // Create and add a new metadata output to the item. let metadataOutput = AVPlayerItemMetadataOutput() @@ -82,6 +91,7 @@ class AVPlayerItemObserver: NSObject { observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context) observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context) observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context) + observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.status, context: &AVPlayerItemObserver.context) // Remove all metadata outputs from the item. observingItem.removeAllMetadataOutputs() @@ -112,7 +122,13 @@ class AVPlayerItemObserver: NSObject { if let playbackLikelyToKeepUp = change?[.newKey] as? Bool { delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp) } - + + case AVPlayerItemKeyPath.status: + if let statusValue = change?[.newKey] as? Int, + let status = AVPlayerItem.Status(rawValue: statusValue) { + delegate?.item(didUpdateStatus: status) + } + default: break }