344 lines
9.9 KiB
Swift
Executable File
344 lines
9.9 KiB
Swift
Executable File
//
|
|
// AVPlayerWrapper.swift
|
|
// SwiftAudio
|
|
//
|
|
// Created by Jørgen Henrichsen on 06/03/2018.
|
|
// Copyright © 2018 Jørgen Henrichsen. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import AVFoundation
|
|
import MediaPlayer
|
|
|
|
public enum PlaybackEndedReason: String {
|
|
case playedUntilEnd
|
|
case playerStopped
|
|
case skippedToNext
|
|
case skippedToPrevious
|
|
case jumpedToIndex
|
|
}
|
|
|
|
class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
|
|
|
struct Constants {
|
|
static let assetPlayableKey = "playable"
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
var avPlayer: AVPlayer
|
|
let playerObserver: AVPlayerObserver
|
|
let playerTimeObserver: AVPlayerTimeObserver
|
|
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
|
|
let playerItemObserver: AVPlayerItemObserver
|
|
|
|
/**
|
|
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
|
*/
|
|
fileprivate var _playWhenReady: Bool = true
|
|
fileprivate var _initialTime: TimeInterval?
|
|
|
|
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
|
didSet {
|
|
if oldValue != _state {
|
|
self.delegate?.AVWrapper(didChangeState: _state)
|
|
}
|
|
}
|
|
}
|
|
|
|
public init() {
|
|
self.avPlayer = AVPlayer()
|
|
self.playerObserver = AVPlayerObserver()
|
|
self.playerObserver.player = avPlayer
|
|
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
|
|
self.playerTimeObserver.player = avPlayer
|
|
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
|
|
self.playerItemObserver = AVPlayerItemObserver()
|
|
|
|
self.playerObserver.delegate = self
|
|
self.playerTimeObserver.delegate = self
|
|
self.playerItemNotificationObserver.delegate = self
|
|
self.playerItemObserver.delegate = self
|
|
|
|
playerTimeObserver.registerForPeriodicTimeEvents()
|
|
}
|
|
|
|
// MARK: - AVPlayerWrapperProtocol
|
|
|
|
var state: AVPlayerWrapperState {
|
|
return _state
|
|
}
|
|
|
|
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
|
|
return avPlayer.reasonForWaitingToPlay
|
|
}
|
|
|
|
var currentItem: AVPlayerItem? {
|
|
return avPlayer.currentItem
|
|
}
|
|
|
|
var _pendingAsset: AVAsset? = nil
|
|
|
|
var automaticallyWaitsToMinimizeStalling: Bool {
|
|
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
|
|
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
|
|
}
|
|
|
|
var currentTime: TimeInterval {
|
|
let seconds = avPlayer.currentTime().seconds
|
|
return seconds.isNaN ? 0 : seconds
|
|
}
|
|
|
|
var duration: TimeInterval {
|
|
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
|
|
return seconds
|
|
}
|
|
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
|
return seconds
|
|
}
|
|
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
|
|
!seconds.isNaN {
|
|
return seconds
|
|
}
|
|
return 0.0
|
|
}
|
|
|
|
var bufferedPosition: TimeInterval {
|
|
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
|
}
|
|
|
|
weak var delegate: AVPlayerWrapperDelegate? = nil
|
|
|
|
var bufferDuration: TimeInterval = 0
|
|
|
|
var timeEventFrequency: TimeEventFrequency = .everySecond {
|
|
didSet {
|
|
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
|
|
}
|
|
}
|
|
|
|
var rate: Float {
|
|
get { return avPlayer.rate }
|
|
set { avPlayer.rate = newValue }
|
|
}
|
|
|
|
var volume: Float {
|
|
get { return avPlayer.volume }
|
|
set { avPlayer.volume = newValue }
|
|
}
|
|
|
|
var isMuted: Bool {
|
|
get { return avPlayer.isMuted }
|
|
set { avPlayer.isMuted = newValue }
|
|
}
|
|
|
|
func play() {
|
|
avPlayer.play()
|
|
}
|
|
|
|
func pause() {
|
|
avPlayer.pause()
|
|
}
|
|
|
|
func togglePlaying() {
|
|
switch avPlayer.timeControlStatus {
|
|
case .playing, .waitingToPlayAtSpecifiedRate:
|
|
pause()
|
|
case .paused:
|
|
play()
|
|
@unknown default:
|
|
fatalError("Unknown AVPlayer.timeControlStatus")
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
pause()
|
|
reset(soft: false)
|
|
}
|
|
|
|
func seek(to seconds: TimeInterval) {
|
|
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
|
|
if let _ = self._initialTime {
|
|
self._initialTime = nil
|
|
if self._playWhenReady {
|
|
self.play()
|
|
}
|
|
}
|
|
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func load(from url: URL, playWhenReady: Bool) {
|
|
reset(soft: true)
|
|
_playWhenReady = playWhenReady
|
|
|
|
if currentItem?.status == .failed {
|
|
recreateAVPlayer()
|
|
}
|
|
|
|
self._pendingAsset = AVURLAsset(url: url)
|
|
|
|
if let pendingAsset = _pendingAsset {
|
|
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
|
|
|
|
guard let self = self else {
|
|
return
|
|
}
|
|
|
|
var error: NSError? = nil
|
|
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
|
|
|
|
DispatchQueue.main.async {
|
|
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
|
|
switch status {
|
|
case .loaded:
|
|
if isPendingAsset {
|
|
let currentItem = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
|
|
currentItem.preferredForwardBufferDuration = self.bufferDuration
|
|
self.avPlayer.replaceCurrentItem(with: currentItem)
|
|
|
|
// Register for events
|
|
self.playerTimeObserver.registerForBoundaryTimeEvents()
|
|
self.playerObserver.startObserving()
|
|
self.playerItemNotificationObserver.startObserving(item: currentItem)
|
|
self.playerItemObserver.startObserving(item: currentItem)
|
|
}
|
|
break
|
|
|
|
case .failed:
|
|
if isPendingAsset {
|
|
self.delegate?.AVWrapper(failedWithError: error)
|
|
self._pendingAsset = nil
|
|
}
|
|
break
|
|
|
|
case .cancelled:
|
|
break
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
|
|
_initialTime = initialTime
|
|
self.pause()
|
|
self.load(from: url, playWhenReady: playWhenReady)
|
|
}
|
|
|
|
// MARK: - Util
|
|
|
|
private func reset(soft: Bool) {
|
|
playerItemObserver.stopObservingCurrentItem()
|
|
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
|
playerItemNotificationObserver.stopObservingCurrentItem()
|
|
|
|
self._pendingAsset?.cancelLoading()
|
|
self._pendingAsset = nil
|
|
|
|
if !soft {
|
|
avPlayer.replaceCurrentItem(with: nil)
|
|
}
|
|
}
|
|
|
|
/// Will recreate the AVPlayer instance. Used when the current one fails.
|
|
private func recreateAVPlayer() {
|
|
let player = AVPlayer()
|
|
playerObserver.player = player
|
|
playerTimeObserver.player = player
|
|
playerTimeObserver.registerForPeriodicTimeEvents()
|
|
avPlayer = player
|
|
delegate?.AVWrapperDidRecreateAVPlayer()
|
|
}
|
|
|
|
}
|
|
|
|
extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
|
|
|
// MARK: - AVPlayerObserverDelegate
|
|
|
|
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
|
switch status {
|
|
case .paused:
|
|
if currentItem == nil {
|
|
_state = .idle
|
|
}
|
|
else {
|
|
self._state = .paused
|
|
}
|
|
case .waitingToPlayAtSpecifiedRate:
|
|
self._state = .loading
|
|
case .playing:
|
|
self._state = .playing
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func player(statusDidChange status: AVPlayer.Status) {
|
|
switch status {
|
|
|
|
case .readyToPlay:
|
|
self._state = .ready
|
|
|
|
if let initialTime = _initialTime {
|
|
self.seek(to: initialTime)
|
|
}
|
|
else if _playWhenReady {
|
|
self.play()
|
|
}
|
|
|
|
break
|
|
|
|
case .failed:
|
|
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
|
|
break
|
|
|
|
case .unknown:
|
|
break
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
|
|
|
|
// MARK: - AVPlayerTimeObserverDelegate
|
|
|
|
func audioDidStart() {
|
|
self._state = .playing
|
|
}
|
|
|
|
func timeEvent(time: CMTime) {
|
|
self.delegate?.AVWrapper(secondsElapsed: time.seconds)
|
|
}
|
|
|
|
}
|
|
|
|
extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
|
|
|
// MARK: - AVPlayerItemNotificationObserverDelegate
|
|
|
|
func itemDidPlayToEndTime() {
|
|
delegate?.AVWrapperItemDidPlayToEndTime()
|
|
}
|
|
|
|
}
|
|
|
|
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
|
|
|
// MARK: - AVPlayerItemObserverDelegate
|
|
|
|
func item(didUpdateDuration duration: Double) {
|
|
self.delegate?.AVWrapper(didUpdateDuration: duration)
|
|
}
|
|
|
|
}
|