Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 52 additions & 12 deletions Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
private let playerItemNotificationObserver = AVPlayerItemNotificationObserver()
private let playerItemObserver = AVPlayerItemObserver()
fileprivate var timeToSeekToAfterLoading: TimeInterval?

/// Last known-good playback position, updated every time the
/// periodic time observer fires or a seek completes. Used to
/// preserve position across reload/recreate cycles.
fileprivate(set) var lastPosition: TimeInterval = 0

fileprivate var asset: AVAsset? = nil
fileprivate var item: AVPlayerItem? = nil
fileprivate var url: URL? = nil
Expand Down Expand Up @@ -83,7 +89,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
public var playWhenReady: Bool = false {
didSet {
if (playWhenReady == true && (state == .failed || state == .stopped)) {
reload(startFromCurrentTime: state == .failed)
reload(startFromCurrentTime: true)
}

applyAVPlayerRate()
Expand All @@ -108,7 +114,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {

var currentTime: TimeInterval {
let seconds = avPlayer.currentTime().seconds
return seconds.isNaN ? 0 : seconds
return seconds.isNaN ? lastPosition : seconds
}

var duration: TimeInterval {
Expand Down Expand Up @@ -196,9 +202,11 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
// if the player is loading then we need to defer seeking until it's ready.
if (avPlayer.currentItem == nil) {
timeToSeekToAfterLoading = seconds
lastPosition = seconds
} else {
let time = CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)
avPlayer.seek(to: time, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) { (finished) in
self.lastPosition = seconds
self.delegate?.AVWrapper(seekTo: Double(seconds), didFinish: finished)
}
}
Expand Down Expand Up @@ -312,6 +320,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {

func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
self.playWhenReady = playWhenReady
// Reset lastPosition when loading a genuinely new URL
if self.url != url {
self.lastPosition = 0
}
self.url = url
self.urlOptions = options
self.load()
Expand Down Expand Up @@ -352,20 +364,14 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {

func unload() {
clearCurrentItem()
lastPosition = 0
state = .idle
}

func reload(startFromCurrentTime: Bool) {
var time : Double? = nil
if (startFromCurrentTime) {
if let currentItem = currentItem {
if (!currentItem.duration.isIndefinite) {
time = currentItem.currentTime().seconds
}
}
}
let time = startFromCurrentTime ? lastPosition : nil
load()
if let time = time {
if let time = time, time > 0 {
seek(to: time)
}
}
Expand Down Expand Up @@ -477,7 +483,11 @@ extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
}

func timeEvent(time: CMTime) {
delegate?.AVWrapper(secondsElapsed: time.seconds)
let seconds = time.seconds
if !seconds.isNaN {
lastPosition = seconds
}
delegate?.AVWrapper(secondsElapsed: seconds)
}

}
Expand All @@ -493,6 +503,28 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
func itemPlaybackStalled() {
delegate?.AVWrapperItemPlaybackStalled()
}

func itemNewErrorLogEntry() {
guard let playerItem = avPlayer.currentItem,
let errorLog = playerItem.errorLog(),
let lastEvent = errorLog.events.last else { return }

var entry: [String: Any] = [:]
entry["errorStatusCode"] = lastEvent.errorStatusCode
entry["errorDomain"] = lastEvent.errorDomain
if let errorComment = lastEvent.errorComment {
entry["errorComment"] = errorComment
}
if let uri = lastEvent.uri {
entry["uri"] = uri
}
if let serverAddress = lastEvent.serverAddress {
entry["serverAddress"] = serverAddress
}
entry["date"] = lastEvent.date?.timeIntervalSince1970 ?? 0

delegate?.AVWrapperItemNewErrorLogEntry(entries: [entry])
}

func itemDidPlayToEndTime() {
delegate?.AVWrapperItemDidPlayToEndTime()
Expand All @@ -516,4 +548,12 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup]) {
delegate?.AVWrapper(didReceiveTimedMetadata: metadata)
}

func item(didChangeBufferEmpty isEmpty: Bool) {
delegate?.AVWrapper(didChangeBufferEmpty: isEmpty)
}

func item(didChangeBufferFull isFull: Bool) {
delegate?.AVWrapper(didChangeBufferFull: isFull)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,8 @@ protocol AVPlayerWrapperDelegate: AnyObject {
func AVWrapperItemDidPlayToEndTime()
func AVWrapperItemFailedToPlayToEndTime()
func AVWrapperItemPlaybackStalled()
func AVWrapperItemNewErrorLogEntry(entries: [[String: Any]])
func AVWrapper(didChangeBufferEmpty isEmpty: Bool)
func AVWrapper(didChangeBufferFull isFull: Bool)
func AVWrapperDidRecreateAVPlayer()
}
29 changes: 22 additions & 7 deletions Sources/SwiftAudioEx/AudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,29 +117,32 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}

/**
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering. Setting `bufferDuration` to larger than zero automatically disables `automaticallyWaitsToMinimizeStalling`. Setting it back to zero automatically enables `automaticallyWaitsToMinimizeStalling`.
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.

Setting `bufferDuration` controls `AVPlayerItem.preferredForwardBufferDuration`
and is independent of `automaticallyWaitsToMinimizeStalling`.

[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
*/
public var bufferDuration: TimeInterval {
get { wrapper.bufferDuration }
set {
wrapper.bufferDuration = newValue
wrapper.automaticallyWaitsToMinimizeStalling = wrapper.bufferDuration == 0
}
}

/**
Indicates whether the player should automatically delay playback in order to minimize stalling. Setting this to true will also set `bufferDuration` back to `0`.
Indicates whether the player should automatically delay playback in order to minimize stalling.

Defaults to `true` (the AVPlayer default). When `true`, the player
waits until it has buffered enough data to play through without
stalling before it begins playback.

[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
*/
public var automaticallyWaitsToMinimizeStalling: Bool {
get { wrapper.automaticallyWaitsToMinimizeStalling }
set {
if (newValue) {
wrapper.bufferDuration = 0
}
wrapper.automaticallyWaitsToMinimizeStalling = newValue
}
}
Expand Down Expand Up @@ -440,9 +443,21 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}

func AVWrapperItemPlaybackStalled() {

event.playbackStalled.emit(data: ())
}

func AVWrapperItemNewErrorLogEntry(entries: [[String: Any]]) {
event.newErrorLogEntry.emit(data: entries)
}

func AVWrapper(didChangeBufferEmpty isEmpty: Bool) {
event.bufferEmpty.emit(data: isEmpty)
}

func AVWrapper(didChangeBufferFull isFull: Bool) {
event.bufferFull.emit(data: isFull)
}

func AVWrapperDidRecreateAVPlayer() {
event.didRecreateAVPlayer.emit(data: ())
}
Expand Down
33 changes: 33 additions & 0 deletions Sources/SwiftAudioEx/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ extension AudioPlayer {
public typealias UpdateDurationEventData = Double
public typealias MetadataCommonEventData = [AVMetadataItem]
public typealias MetadataTimedEventData = [AVTimedMetadataGroup]
public typealias PlaybackStalledEventData = ()
public typealias NewErrorLogEntryEventData = [[String: Any]]
public typealias BufferEmptyEventData = Bool
public typealias BufferFullEventData = Bool
public typealias DidRecreateAVPlayerEventData = ()
public typealias CurrentItemEventData = (
item: AudioItem?,
Expand Down Expand Up @@ -91,6 +95,35 @@ extension AudioPlayer {
*/
public let receiveChapterMetadata: AudioPlayer.Event<MetadataTimedEventData> = AudioPlayer.Event()

/**
Emitted when playback has stalled due to buffer underrun.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: This indicates the player's buffer ran empty during playback and audio output has temporarily stopped.
*/
public let playbackStalled: AudioPlayer.Event<PlaybackStalledEventData> = AudioPlayer.Event()

/**
Emitted when a new error log entry is added by AVPlayer (iOS only).
Contains an array of dictionaries with error log entry details such as errorStatusCode, errorDomain, errorComment, and URI.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: These are transient streaming errors (e.g. segment download failures) that may not stop playback.
*/
public let newErrorLogEntry: AudioPlayer.Event<NewErrorLogEntryEventData> = AudioPlayer.Event()

/**
Emitted when the playback buffer becomes empty.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: When true, playback will stall or has stalled because the buffer has no more data to play.
*/
public let bufferEmpty: AudioPlayer.Event<BufferEmptyEventData> = AudioPlayer.Event()

/**
Emitted when the playback buffer becomes full.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: When true, the internal buffer is full and no more data will be downloaded until some is consumed.
*/
public let bufferFull: AudioPlayer.Event<BufferFullEventData> = AudioPlayer.Event()

/**
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
func itemDidPlayToEndTime()
func itemFailedToPlayToEndTime()
func itemPlaybackStalled()
func itemNewErrorLogEntry()
}

/**
Expand Down Expand Up @@ -60,6 +61,12 @@ class AVPlayerItemNotificationObserver {
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: item
)
notificationCenter.addObserver(
self,
selector: #selector(itemNewErrorLogEntry),
name: NSNotification.Name.AVPlayerItemNewErrorLogEntry,
object: item
)
}

/**
Expand All @@ -84,6 +91,11 @@ class AVPlayerItemNotificationObserver {
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: observingItem
)
notificationCenter.removeObserver(
self,
name: NSNotification.Name.AVPlayerItemNewErrorLogEntry,
object: observingItem
)
self.observingItem = nil
isObserving = false
}
Expand All @@ -99,4 +111,8 @@ class AVPlayerItemNotificationObserver {
@objc private func itemPlaybackStalled() {
delegate?.itemPlaybackStalled()
}

@objc private func itemNewErrorLogEntry() {
delegate?.itemNewErrorLogEntry()
}
}
29 changes: 27 additions & 2 deletions Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ protocol AVPlayerItemObserverDelegate: AnyObject {
Called when the observed item receives metadata
*/
func item(didReceiveTimedMetadata metadata: [AVTimedMetadataGroup])


/**
Called when the playback buffer empty state changes.
*/
func item(didChangeBufferEmpty isEmpty: Bool)

/**
Called when the playback buffer full state changes.
*/
func item(didChangeBufferFull isFull: Bool)
}

/**
Expand All @@ -38,6 +47,8 @@ class AVPlayerItemObserver: NSObject {
static let duration = #keyPath(AVPlayerItem.duration)
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
static let playbackLikelyToKeepUp = #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)
static let playbackBufferEmpty = #keyPath(AVPlayerItem.isPlaybackBufferEmpty)
static let playbackBufferFull = #keyPath(AVPlayerItem.isPlaybackBufferFull)
}

private(set) var isObserving: Bool = false
Expand Down Expand Up @@ -66,6 +77,8 @@ 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.playbackBufferEmpty, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackBufferFull, options: [.new], context: &AVPlayerItemObserver.context)

// Create and add a new metadata output to the item.
let metadataOutput = AVPlayerItemMetadataOutput()
Expand All @@ -82,6 +95,8 @@ 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.playbackBufferEmpty, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackBufferFull, context: &AVPlayerItemObserver.context)

// Remove all metadata outputs from the item.
observingItem.removeAllMetadataOutputs()
Expand Down Expand Up @@ -112,7 +127,17 @@ class AVPlayerItemObserver: NSObject {
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
delegate?.item(didUpdatePlaybackLikelyToKeepUp: playbackLikelyToKeepUp)
}


case AVPlayerItemKeyPath.playbackBufferEmpty:
if let isEmpty = change?[.newKey] as? Bool {
delegate?.item(didChangeBufferEmpty: isEmpty)
}

case AVPlayerItemKeyPath.playbackBufferFull:
if let isFull = change?[.newKey] as? Bool {
delegate?.item(didChangeBufferFull: isFull)
}

default: break

}
Expand Down