2022-03-07 20:46:59 +01:00
|
|
|
//
|
|
|
|
// AudioPlayer.swift
|
|
|
|
// App
|
|
|
|
//
|
|
|
|
// Created by Rasmus Krämer on 07.03.22.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import AVFoundation
|
|
|
|
import UIKit
|
|
|
|
import MediaPlayer
|
|
|
|
|
|
|
|
class AudioPlayer: NSObject {
|
|
|
|
// enums and @objc are not compatible
|
|
|
|
@objc dynamic var status: Int
|
|
|
|
@objc dynamic var rate: Float
|
2022-03-22 15:38:31 +01:00
|
|
|
|
2022-03-09 22:10:15 +01:00
|
|
|
private var tmpRate: Float = 1.0
|
2022-03-22 15:38:31 +01:00
|
|
|
private var lastPlayTime: Double = 0.0
|
2022-03-07 20:46:59 +01:00
|
|
|
|
|
|
|
private var playerContext = 0
|
|
|
|
private var playerItemContext = 0
|
|
|
|
|
|
|
|
private var playWhenReady: Bool
|
2022-04-23 15:40:18 -05:00
|
|
|
private var initialPlaybackRate: Float
|
2022-03-07 20:46:59 +01:00
|
|
|
|
|
|
|
private var audioPlayer: AVPlayer
|
2022-04-12 14:28:47 +02:00
|
|
|
private var playbackSession: PlaybackSession
|
|
|
|
private var activeAudioTrack: AudioTrack
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-03-28 22:09:43 +02:00
|
|
|
// MARK: - Constructor
|
2022-04-23 15:40:18 -05:00
|
|
|
init(playbackSession: PlaybackSession, playWhenReady: Bool = false, playbackRate: Float = 1) {
|
2022-03-07 20:46:59 +01:00
|
|
|
self.playWhenReady = playWhenReady
|
2022-04-23 15:40:18 -05:00
|
|
|
self.initialPlaybackRate = playbackRate
|
2022-03-07 20:46:59 +01:00
|
|
|
self.audioPlayer = AVPlayer()
|
2022-04-12 14:28:47 +02:00
|
|
|
self.playbackSession = playbackSession
|
2022-03-07 20:46:59 +01:00
|
|
|
self.status = -1
|
|
|
|
self.rate = 0.0
|
2022-04-23 15:40:18 -05:00
|
|
|
self.tmpRate = playbackRate
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-04-12 14:28:47 +02:00
|
|
|
if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" {
|
|
|
|
NSLog("The player only support HLS streams right now")
|
2022-04-25 00:15:44 -04:00
|
|
|
self.activeAudioTrack = AudioTrack(index: 0, startOffset: -1, duration: -1, title: "", contentUrl: nil, mimeType: "", metadata: nil, serverIndex: 0)
|
2022-04-12 14:28:47 +02:00
|
|
|
|
|
|
|
super.init()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self.activeAudioTrack = playbackSession.audioTracks[0]
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
super.init()
|
|
|
|
|
|
|
|
initAudioSession()
|
|
|
|
setupRemoteTransportControls()
|
|
|
|
|
|
|
|
// Listen to player events
|
|
|
|
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext)
|
|
|
|
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext)
|
|
|
|
|
|
|
|
let playerItem = AVPlayerItem(asset: createAsset())
|
|
|
|
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: &playerItemContext)
|
|
|
|
|
|
|
|
self.audioPlayer.replaceCurrentItem(with: playerItem)
|
|
|
|
|
|
|
|
NSLog("Audioplayer ready")
|
|
|
|
}
|
|
|
|
deinit {
|
|
|
|
destroy()
|
|
|
|
}
|
2022-03-28 22:09:43 +02:00
|
|
|
public func destroy() {
|
2022-04-28 18:05:33 -05:00
|
|
|
// Pause is not synchronous causing this error on below lines:
|
|
|
|
// AVAudioSession_iOS.mm:1206 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session
|
2022-03-07 20:46:59 +01:00
|
|
|
pause()
|
2022-03-22 15:38:31 +01:00
|
|
|
audioPlayer.replaceCurrentItem(with: nil)
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-03-22 15:38:31 +01:00
|
|
|
do {
|
|
|
|
try AVAudioSession.sharedInstance().setActive(false)
|
|
|
|
} catch {
|
|
|
|
NSLog("Failed to set AVAudioSession inactive")
|
|
|
|
print(error)
|
|
|
|
}
|
|
|
|
|
2022-04-28 18:05:33 -05:00
|
|
|
// Throws error Possibly related to the error above
|
2022-04-23 15:40:18 -05:00
|
|
|
// DispatchQueue.main.sync {
|
2022-04-28 18:05:33 -05:00
|
|
|
// UIApplication.shared.endReceivingRemoteControlEvents()
|
2022-04-23 15:40:18 -05:00
|
|
|
// }
|
2022-04-28 18:05:33 -05:00
|
|
|
|
2022-04-14 14:39:09 +02:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Methods
|
2022-03-22 15:38:31 +01:00
|
|
|
public func play(allowSeekBack: Bool = false) {
|
|
|
|
if allowSeekBack {
|
2022-03-23 16:13:28 +01:00
|
|
|
let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime
|
|
|
|
var time: Int?
|
|
|
|
|
|
|
|
if lastPlayTime == 0 {
|
|
|
|
time = 5
|
|
|
|
} else if diffrence < 6 {
|
|
|
|
time = 2
|
|
|
|
} else if diffrence < 12 {
|
|
|
|
time = 10
|
|
|
|
} else if diffrence < 30 {
|
|
|
|
time = 15
|
|
|
|
} else if diffrence < 180 {
|
|
|
|
time = 20
|
|
|
|
} else if diffrence < 3600 {
|
|
|
|
time = 25
|
|
|
|
} else {
|
|
|
|
time = 29
|
|
|
|
}
|
|
|
|
|
|
|
|
if time != nil {
|
|
|
|
seek(getCurrentTime() - Double(time!))
|
2022-03-22 15:38:31 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
self.audioPlayer.play()
|
|
|
|
self.status = 1
|
2022-03-09 22:10:15 +01:00
|
|
|
self.rate = self.tmpRate
|
|
|
|
self.audioPlayer.rate = self.tmpRate
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
updateNowPlaying()
|
|
|
|
}
|
|
|
|
public func pause() {
|
|
|
|
self.audioPlayer.pause()
|
|
|
|
self.status = 0
|
|
|
|
self.rate = 0.0
|
|
|
|
|
|
|
|
updateNowPlaying()
|
2022-03-22 15:38:31 +01:00
|
|
|
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
public func seek(_ to: Double) {
|
|
|
|
let continuePlaing = rate > 0.0
|
|
|
|
|
|
|
|
pause()
|
|
|
|
self.audioPlayer.seek(to: CMTime(seconds: to, preferredTimescale: 1000)) { completed in
|
|
|
|
if !completed {
|
|
|
|
NSLog("WARNING: seeking not completed (to \(to)")
|
|
|
|
}
|
|
|
|
|
|
|
|
if continuePlaing {
|
|
|
|
self.play()
|
|
|
|
}
|
|
|
|
self.updateNowPlaying()
|
|
|
|
}
|
|
|
|
}
|
2022-03-09 22:10:15 +01:00
|
|
|
|
|
|
|
public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
|
|
|
|
if self.audioPlayer.rate != rate {
|
2022-03-07 20:46:59 +01:00
|
|
|
self.audioPlayer.rate = rate
|
|
|
|
}
|
2022-03-09 22:10:15 +01:00
|
|
|
if rate > 0.0 && !(observed && rate == 1) {
|
|
|
|
self.tmpRate = rate
|
|
|
|
}
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
self.rate = rate
|
|
|
|
|
|
|
|
self.updateNowPlaying()
|
|
|
|
}
|
|
|
|
|
|
|
|
public func getCurrentTime() -> Double {
|
|
|
|
self.audioPlayer.currentTime().seconds
|
|
|
|
}
|
|
|
|
public func getDuration() -> Double {
|
|
|
|
self.audioPlayer.currentItem?.duration.seconds ?? 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
private func createAsset() -> AVAsset {
|
|
|
|
let headers: [String: String] = [
|
2022-04-15 10:16:11 +02:00
|
|
|
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
2022-03-07 20:46:59 +01:00
|
|
|
]
|
|
|
|
|
2022-04-25 00:15:44 -04:00
|
|
|
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
private func initAudioSession() {
|
|
|
|
do {
|
|
|
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay])
|
|
|
|
try AVAudioSession.sharedInstance().setActive(true)
|
|
|
|
} catch {
|
|
|
|
NSLog("Failed to set AVAudioSession category")
|
|
|
|
print(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Now playing
|
2022-03-28 22:09:43 +02:00
|
|
|
private func setupRemoteTransportControls() {
|
2022-04-14 12:24:27 +02:00
|
|
|
// DispatchQueue.main.sync {
|
2022-03-22 15:38:31 +01:00
|
|
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
2022-04-14 12:24:27 +02:00
|
|
|
// }
|
2022-03-07 20:46:59 +01:00
|
|
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
|
|
|
|
|
|
commandCenter.playCommand.isEnabled = true
|
|
|
|
commandCenter.playCommand.addTarget { [unowned self] event in
|
2022-03-22 15:38:31 +01:00
|
|
|
play(allowSeekBack: true)
|
2022-03-07 20:46:59 +01:00
|
|
|
return .success
|
|
|
|
}
|
|
|
|
commandCenter.pauseCommand.isEnabled = true
|
|
|
|
commandCenter.pauseCommand.addTarget { [unowned self] event in
|
|
|
|
pause()
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
|
|
|
commandCenter.skipForwardCommand.isEnabled = true
|
|
|
|
commandCenter.skipForwardCommand.preferredIntervals = [30]
|
|
|
|
commandCenter.skipForwardCommand.addTarget { [unowned self] event in
|
|
|
|
guard let command = event.command as? MPSkipIntervalCommand else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
|
|
|
|
|
|
|
seek(getCurrentTime() + command.preferredIntervals[0].doubleValue)
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
commandCenter.skipBackwardCommand.isEnabled = true
|
|
|
|
commandCenter.skipBackwardCommand.preferredIntervals = [30]
|
|
|
|
commandCenter.skipBackwardCommand.addTarget { [unowned self] event in
|
|
|
|
guard let command = event.command as? MPSkipIntervalCommand else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
|
|
|
|
|
|
|
seek(getCurrentTime() - command.preferredIntervals[0].doubleValue)
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
|
|
|
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
|
|
|
commandCenter.changePlaybackPositionCommand.addTarget { event in
|
|
|
|
guard let event = event as? MPChangePlaybackPositionCommandEvent else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
|
|
|
|
|
|
|
self.seek(event.positionTime)
|
|
|
|
return .success
|
|
|
|
}
|
2022-03-22 15:38:31 +01:00
|
|
|
|
|
|
|
commandCenter.changePlaybackRateCommand.isEnabled = true
|
2022-03-22 16:56:59 +01:00
|
|
|
commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2]
|
2022-03-22 15:38:31 +01:00
|
|
|
commandCenter.changePlaybackRateCommand.addTarget { event in
|
|
|
|
guard let event = event as? MPChangePlaybackRateCommandEvent else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
|
|
|
|
|
|
|
self.setPlaybackRate(event.playbackRate)
|
|
|
|
return .success
|
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2022-03-28 22:09:43 +02:00
|
|
|
private func updateNowPlaying() {
|
2022-04-14 14:39:09 +02:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
2022-03-22 16:56:59 +01:00
|
|
|
NowPlayingInfo.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate)
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Observer
|
|
|
|
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
|
|
|
if context == &playerItemContext {
|
|
|
|
if keyPath == #keyPath(AVPlayer.status) {
|
|
|
|
guard let playerStatus = AVPlayerItem.Status(rawValue: (change?[.newKey] as? Int ?? -1)) else { return }
|
|
|
|
|
|
|
|
if playerStatus == .readyToPlay {
|
2022-03-28 22:09:43 +02:00
|
|
|
self.updateNowPlaying()
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-04-16 10:35:48 -05:00
|
|
|
let firstReady = self.status < 0
|
2022-03-07 20:46:59 +01:00
|
|
|
self.status = 0
|
|
|
|
if self.playWhenReady {
|
2022-04-16 10:35:48 -05:00
|
|
|
seek(playbackSession.currentTime)
|
2022-03-07 20:46:59 +01:00
|
|
|
self.playWhenReady = false
|
|
|
|
self.play()
|
2022-04-16 10:35:48 -05:00
|
|
|
} else if (firstReady) { // Only seek on first readyToPlay
|
|
|
|
seek(playbackSession.currentTime)
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if context == &playerContext {
|
|
|
|
if keyPath == #keyPath(AVPlayer.rate) {
|
2022-03-28 22:09:43 +02:00
|
|
|
self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
|
2022-03-07 20:46:59 +01:00
|
|
|
} else if keyPath == #keyPath(AVPlayer.currentItem) {
|
|
|
|
NSLog("WARNING: Item ended")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2022-03-28 22:09:43 +02:00
|
|
|
|
|
|
|
public static var instance: AudioPlayer?
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|