Merge branch 'bug-ios-sleep-timer' into ios-audio-events

This commit is contained in:
ronaldheft 2022-09-03 16:53:03 -04:00
commit d1c1902cd3
8 changed files with 351 additions and 284 deletions

View file

@ -58,6 +58,7 @@
E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */; }; E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */; };
E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507228AC218300C746DD /* DaoExtensions.swift */; }; E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507228AC218300C746DD /* DaoExtensions.swift */; };
E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507428AEF93100C746DD /* PlayerSettings.swift */; }; E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507428AEF93100C746DD /* PlayerSettings.swift */; };
E9DFCBFB28C28F4A00B36356 /* AudioPlayerSleepTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */; };
E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E985F728B02D9400957F23 /* PlayerProgress.swift */; }; E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E985F728B02D9400957F23 /* PlayerProgress.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -117,6 +118,7 @@
E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItemPart.swift; sourceTree = "<group>"; }; E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItemPart.swift; sourceTree = "<group>"; };
E9D5507228AC218300C746DD /* DaoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaoExtensions.swift; sourceTree = "<group>"; }; E9D5507228AC218300C746DD /* DaoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaoExtensions.swift; sourceTree = "<group>"; };
E9D5507428AEF93100C746DD /* PlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettings.swift; sourceTree = "<group>"; }; E9D5507428AEF93100C746DD /* PlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettings.swift; sourceTree = "<group>"; };
E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSleepTimer.swift; sourceTree = "<group>"; };
E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = "<group>"; }; E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -145,6 +147,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */, 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */,
E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */,
3ABF618E2804325C0070250E /* PlayerHandler.swift */, 3ABF618E2804325C0070250E /* PlayerHandler.swift */,
E9E985F728B02D9400957F23 /* PlayerProgress.swift */, E9E985F728B02D9400957F23 /* PlayerProgress.swift */,
); );
@ -438,6 +441,7 @@
E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */, E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */,
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */, E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */,
E9D5504E28AC1B0700C746DD /* AudioFile.swift in Sources */, E9D5504E28AC1B0700C746DD /* AudioFile.swift in Sources */,
E9DFCBFB28C28F4A00B36356 /* AudioPlayerSleepTimer.swift in Sources */,
E9D5505428AC1B7900C746DD /* AudioTrack.swift in Sources */, E9D5505428AC1B7900C746DD /* AudioTrack.swift in Sources */,
E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */, E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */,
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */, 4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */,

View file

@ -6,6 +6,7 @@ import RealmSwift
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
var backgroundCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch. // Override point for customization after application launch.
@ -72,6 +73,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// tracking app url opens, make sure to keep this call // tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
} }
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
// Stores the completion handler for background downloads
// The identifier of this method can be ignored at this time as we only have one background url session
backgroundCompletionHandler = completionHandler
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event) super.touchesBegan(touches, with: event)

View file

@ -169,7 +169,7 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func decreaseSleepTime(_ call: CAPPluginCall) { @objc func decreaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) }
let seconds = time/1000 let seconds = time/1000
PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds) PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds)
@ -179,7 +179,7 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func increaseSleepTime(_ call: CAPPluginCall) { @objc func increaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) }
let seconds = time/1000 let seconds = time/1000
PlayerHandler.increaseSleepTime(increaseSeconds: seconds) PlayerHandler.increaseSleepTime(increaseSeconds: seconds)
@ -188,30 +188,29 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func setSleepTimer(_ call: CAPPluginCall) { @objc func setSleepTimer(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
let isChapterTime = call.getBool("isChapterTime", false) let isChapterTime = call.getBool("isChapterTime", false)
let seconds = time / 1000 let seconds = time / 1000
NSLog("chapter time: \(isChapterTime)") NSLog("chapter time: \(isChapterTime)")
if isChapterTime { if isChapterTime {
PlayerHandler.setChapterSleepTime(stopAt: Double(seconds)) PlayerHandler.setChapterSleepTime(stopAt: seconds)
return call.resolve([ "success": true ]) return call.resolve([ "success": true ])
} else {
PlayerHandler.setSleepTime(secondsUntilSleep: seconds)
call.resolve([ "success": true ])
} }
PlayerHandler.setSleepTime(secondsUntilSleep: Double(seconds))
call.resolve([ "success": true ])
} }
@objc func cancelSleepTimer(_ call: CAPPluginCall) { @objc func cancelSleepTimer(_ call: CAPPluginCall) {
PlayerHandler.cancelSleepTime() PlayerHandler.cancelSleepTime()
PlayerHandler.sleepTimerChapterStopTime = nil
call.resolve() call.resolve()
} }
@objc func getSleepTimerTime(_ call: CAPPluginCall) { @objc func getSleepTimerTime(_ call: CAPPluginCall) {
call.resolve([ call.resolve([
"value": PlayerHandler.remainingSleepTime "value": PlayerHandler.getSleepTimeRemaining()
]) ])
} }
@ -223,7 +222,7 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func sendSleepTimerSet() { @objc func sendSleepTimerSet() {
self.notifyListeners("onSleepTimerSet", data: [ self.notifyListeners("onSleepTimerSet", data: [
"value": PlayerHandler.remainingSleepTime "value": PlayerHandler.getSleepTimeRemaining()
]) ])
} }

View file

@ -15,9 +15,10 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
private lazy var session: URLSession = { private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader")
let queue = OperationQueue() let queue = OperationQueue()
queue.maxConcurrentOperationCount = 5 queue.maxConcurrentOperationCount = 5
return URLSession(configuration: .default, delegate: self, delegateQueue: queue) return URLSession(configuration: config, delegate: self, delegateQueue: queue)
}() }()
private let progressStatusQueue = DispatchQueue(label: "progress-status-queue", attributes: .concurrent) private let progressStatusQueue = DispatchQueue(label: "progress-status-queue", attributes: .concurrent)
private var downloadItemProgress = [String: DownloadItem]() private var downloadItemProgress = [String: DownloadItem]()
@ -78,6 +79,18 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
} }
} }
// Called when downloads are complete on the background thread
public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let backgroundCompletionHandler =
appDelegate.backgroundCompletionHandler else {
return
}
backgroundCompletionHandler()
}
}
private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) { private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) {
do { do {
guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription } guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription }

View file

@ -19,6 +19,13 @@ class AudioTrack: EmbeddedObject, Codable {
@Persisted var localFileId: String? @Persisted var localFileId: String?
@Persisted var serverIndex: Int? @Persisted var serverIndex: Int?
var endOffset: Double? {
if let startOffset = startOffset {
return startOffset + duration
}
return nil
}
private enum CodingKeys : String, CodingKey { private enum CodingKeys : String, CodingKey {
case index, startOffset, duration, title, contentUrl, mimeType, metadata, localFileId, serverIndex case index, startOffset, duration, title, contentUrl, mimeType, metadata, localFileId, serverIndex
} }

View file

@ -18,7 +18,7 @@ enum PlayMethod:Int {
} }
class AudioPlayer: NSObject { class AudioPlayer: NSObject {
private let queue = DispatchQueue(label: "ABSAudioPlayerQueue") internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
// enums and @objc are not compatible // enums and @objc are not compatible
@objc dynamic var status: Int @objc dynamic var status: Int
@ -32,17 +32,20 @@ class AudioPlayer: NSObject {
private var playWhenReady: Bool private var playWhenReady: Bool
private var initialPlaybackRate: Float private var initialPlaybackRate: Float
private var audioPlayer: AVQueuePlayer internal var audioPlayer: AVQueuePlayer
private var sessionId: String private var sessionId: String
private var timeObserverToken: Any? private var timeObserverToken: Any?
private var queueObserver:NSKeyValueObservation? private var queueObserver:NSKeyValueObservation?
private var queueItemStatusObserver:NSKeyValueObservation? private var queueItemStatusObserver:NSKeyValueObservation?
private var sleepTimeStopAt: Double? // Sleep timer values
private var sleepTimeToken: Any? internal var sleepTimeChapterStopAt: Double?
internal var sleepTimeChapterToken: Any?
internal var sleepTimer: Timer?
internal var sleepTimeRemaining: Double?
private var currentTrackIndex = 0 internal var currentTrackIndex = 0
private var allPlayerItems:[AVPlayerItem] = [] private var allPlayerItems:[AVPlayerItem] = []
private var pausedTimer: Timer? private var pausedTimer: Timer?
@ -98,20 +101,21 @@ class AudioPlayer: NSObject {
NSLog("Audioplayer ready") NSLog("Audioplayer ready")
} }
deinit { deinit {
self.stopPausedTimer() self.stopPausedTimer()
self.removeSleepTimer() self.removeSleepTimer()
self.removeTimeObserver() self.removeTimeObserver()
self.queueObserver?.invalidate() self.queueObserver?.invalidate()
self.queueItemStatusObserver?.invalidate() self.queueItemStatusObserver?.invalidate()
destroy()
} }
public func destroy() { public func destroy() {
// Pause is not synchronous causing this error on below lines: // 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 // 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
// It is related to L79 `AVAudioSession.sharedInstance().setActive(false)` // It is related to L79 `AVAudioSession.sharedInstance().setActive(false)`
pause() self.pause()
audioPlayer.replaceCurrentItem(with: nil) self.audioPlayer.replaceCurrentItem(with: nil)
do { do {
try AVAudioSession.sharedInstance().setActive(false) try AVAudioSession.sharedInstance().setActive(false)
@ -124,6 +128,11 @@ class AudioPlayer: NSObject {
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
UIApplication.shared.endReceivingRemoteControlEvents() UIApplication.shared.endReceivingRemoteControlEvents()
} }
// Remove observers
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &playerContext)
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), context: &playerContext)
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
} }
@ -178,9 +187,14 @@ class AudioPlayer: NSObject {
await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false) await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false)
} }
// Update the sleep time, if set if self.isSleepTimerSet() {
if self.sleepTimeStopAt != nil { // Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
// Handle a sitation where the user skips past the chapter end
if self.isChapterSleepTimerBeforeTime(currentTime) {
self.removeSleepTimer()
}
} }
} }
} }
@ -194,7 +208,8 @@ class AudioPlayer: NSObject {
} }
private func setupQueueObserver() { private func setupQueueObserver() {
self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) {_,_ in self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) { [weak self] _,_ in
guard let self = self else { return }
let prevTrackIndex = self.currentTrackIndex let prevTrackIndex = self.currentTrackIndex
self.audioPlayer.currentItem.map { item in self.audioPlayer.currentItem.map { item in
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0 self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
@ -206,35 +221,49 @@ class AudioPlayer: NSObject {
} }
private func setupQueueItemStatusObserver() { private func setupQueueItemStatusObserver() {
NSLog("queueStatusObserver: Setting up")
// Listen for player item updates
self.queueItemStatusObserver?.invalidate() self.queueItemStatusObserver?.invalidate()
self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { [weak self] playerItem, change in
guard let playbackSession = self.getPlaybackSession() else { self?.handleQueueItemStatus(playerItem: playerItem)
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
return
}
if (playerItem.status == .readyToPlay) {
NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
self.updateNowPlaying()
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
let firstReady = self.status < 0
if firstReady || self.playWhenReady {
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
}
// Mark the player as ready
self.status = 0
// Start the player, if requested
if self.playWhenReady {
self.playWhenReady = false
self.play()
}
} else if (playerItem.status == .failed) {
NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
}
}) })
// Ensure we didn't miss a player item update during initialization
if let playerItem = self.audioPlayer.currentItem {
self.handleQueueItemStatus(playerItem: playerItem)
}
}
private func handleQueueItemStatus(playerItem: AVPlayerItem) {
NSLog("queueStatusObserver: Current item status changed")
guard let playbackSession = self.getPlaybackSession() else {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
return
}
if (playerItem.status == .readyToPlay) {
NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
let firstReady = self.status < 0
if firstReady && !self.playWhenReady {
// Seek is async, and if we call this when also pressing play, we will get weird jumps in the scrub bar depending on timing
// Seeking to the correct position happens during play()
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
}
// Mark the player as ready
self.status = 0
// Start the player, if requested
if self.playWhenReady {
self.playWhenReady = false
self.play()
}
} else if (playerItem.status == .failed) {
NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
}
} }
private func startPausedTimer() { private func startPausedTimer() {
@ -255,65 +284,71 @@ class AudioPlayer: NSObject {
// MARK: - Methods // MARK: - Methods
public func play(allowSeekBack: Bool = false) { public func play(allowSeekBack: Bool = false) {
guard self.isInitialized() else { return } guard self.isInitialized() else { return }
guard let session = self.getPlaybackSession() else {
// Capture remaining sleep time before changing the track position NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime return
if allowSeekBack, let session = self.getPlaybackSession() {
let lastPlayed = (session.updatedAt ?? 0)/1000
let difference = Date.timeIntervalSinceReferenceDate - lastPlayed
var time: Int?
if lastPlayed == 0 {
time = 5
} else if difference < 6 {
time = 2
} else if difference < 12 {
time = 10
} else if difference < 30 {
time = 15
} else if difference < 180 {
time = 20
} else if difference < 3600 {
time = 25
} else {
time = 29
}
if time != nil {
guard let currentTime = self.getCurrentTime() else {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
return
}
seek(currentTime - Double(time!), from: "play")
}
} }
// Determine where we are starting playback
let lastPlayed = (session.updatedAt ?? 0)/1000
let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime
// Sync our new playback position
Task { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) }
// Start playback, with a seek, for as smooth a scrub bar start as possible
let currentTrackStartOffset = session.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let seekTime = currentTime - currentTrackStartOffset
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in
guard completed else { return }
self?.resumePlayback()
}
}
private func calculateSeekBackTimeAtCurrentTime(_ currentTime: Double, lastPlayed: Double) -> Double {
let difference = Date.timeIntervalSinceReferenceDate - lastPlayed
var time: Double = 0
// Scale seek back time based on how long since last play
if lastPlayed == 0 {
time = 5
} else if difference < 6 {
time = 2
} else if difference < 12 {
time = 10
} else if difference < 30 {
time = 15
} else if difference < 180 {
time = 20
} else if difference < 3600 {
time = 25
} else {
time = 29
}
// Wind the clock back
return currentTime - time
}
private func resumePlayback() {
NSLog("PLAY: Resuming playback")
// Stop the paused timer
self.stopPausedTimer() self.stopPausedTimer()
Task {
if let currentTime = self.getCurrentTime() {
await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false)
}
}
self.markAudioSessionAs(active: true) self.markAudioSessionAs(active: true)
self.audioPlayer.play() self.audioPlayer.play()
self.status = 1
self.rate = self.tmpRate
self.audioPlayer.rate = self.tmpRate self.audioPlayer.rate = self.tmpRate
self.status = 1
// If we have an active sleep timer, reschedule based on rate // Update the progress
if let currentTime = self.getCurrentTime() { self.updateNowPlaying()
self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
}
updateNowPlaying()
} }
public func pause() { public func pause() {
guard self.isInitialized() else { return } guard self.isInitialized() else { return }
NSLog("PAUSE: Pausing playback")
self.audioPlayer.pause() self.audioPlayer.pause()
self.markAudioSessionAs(active: false) self.markAudioSessionAs(active: false)
@ -324,7 +359,6 @@ class AudioPlayer: NSObject {
} }
self.status = 0 self.status = 0
self.rate = 0.0
updateNowPlaying() updateNowPlaying()
@ -334,22 +368,19 @@ class AudioPlayer: NSObject {
public func seek(_ to: Double, from: String) { public func seek(_ to: Double, from: String) {
let continuePlaying = rate > 0.0 let continuePlaying = rate > 0.0
pause() self.pause()
NSLog("Seek to \(to) from \(from)") NSLog("SEEK: Seek to \(to) from \(from)")
guard let playbackSession = self.getPlaybackSession() else { return } guard let playbackSession = self.getPlaybackSession() else { return }
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex] let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
let ctso = currentTrack.startOffset ?? 0.0 let ctso = currentTrack.startOffset ?? 0.0
let trackEnd = ctso + currentTrack.duration let trackEnd = ctso + currentTrack.duration
NSLog("Seek current track END = \(trackEnd)") NSLog("SEEK: Seek current track END = \(trackEnd)")
// Capture remaining sleep time before changing the track position
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
let indexOfSeek = getItemIndexForTime(time: to) let indexOfSeek = getItemIndexForTime(time: to)
NSLog("Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") NSLog("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
// Reconstruct queue if seeking to a different track // Reconstruct queue if seeking to a different track
if (self.currentTrackIndex != indexOfSeek) { if (self.currentTrackIndex != indexOfSeek) {
@ -370,32 +401,24 @@ class AudioPlayer: NSObject {
setupQueueItemStatusObserver() setupQueueItemStatusObserver()
} else { } else {
NSLog("Seeking in current item \(to)") NSLog("SEEK: Seeking in current item \(to)")
let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0 let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let seekTime = to - currentTrackStartOffset let seekTime = to - currentTrackStartOffset
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
if !completed { guard completed else { return NSLog("SEEK: WARNING: seeking not completed (to \(seekTime)") }
NSLog("WARNING: seeking not completed (to \(seekTime)") guard let self = self else { return }
}
if continuePlaying { if continuePlaying {
self?.play() self.resumePlayback()
} }
self?.updateNowPlaying()
// If we have an active sleep timer, reschedule based on seek, since seek is fuzzy self.updateNowPlaying()
// This needs to occur after play() to capture the correct playback rate
if let currentTime = self?.getCurrentTime() {
self?.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
}
} }
} }
} }
public func setPlaybackRate(_ rate: Float, observed: Bool = false) { public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
// Capture remaining sleep time before changing the rate
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1) let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1)
if self.audioPlayer.rate != rate { if self.audioPlayer.rate != rate {
@ -409,124 +432,11 @@ class AudioPlayer: NSObject {
if playbackSpeedChanged { if playbackSpeedChanged {
self.tmpRate = rate self.tmpRate = rate
// If we have an active sleep timer, reschedule based on rate
if let currentTime = self.getCurrentTime() {
self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
}
// Setup the time observer again at the new rate // Setup the time observer again at the new rate
self.setupTimeObserver() self.setupTimeObserver()
} }
} }
public func getSleepStopAt() -> Double? {
return self.sleepTimeStopAt
}
// Let iOS handle the sleep timer logic by letting us know when it's time to stop
public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) {
NSLog("SLEEP TIMER: Scheduling for \(stopAt)")
// Reset any previous sleep timer
self.removeSleepTimer()
guard let currentTime = getCurrentTime() else {
NSLog("Failed to get currenTime")
return
}
// Mark the time to stop playing
if scaleBasedOnSpeed {
// Consider paused as playing at 1x
let rate = Double(self.rate > 0 ? self.rate : 1)
// Calculate the scaled time to stop at
let timeUntilSleep = (stopAt - currentTime) * rate
self.sleepTimeStopAt = currentTime + timeUntilSleep
NSLog("SLEEP TIMER: Adjusted based on playback speed of \(rate) to \(self.sleepTimeStopAt!)")
} else {
self.sleepTimeStopAt = stopAt
}
guard let sleepTimeStopAt = self.sleepTimeStopAt else { return }
let sleepTime = CMTime(seconds: sleepTimeStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Schedule the observation time
var times = [NSValue]()
times.append(NSValue(time: sleepTime))
sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: queue) { [weak self] in
NSLog("SLEEP TIMER: Pausing audio")
self?.pause()
PlayerHandler.sleepTimerChapterStopTime = nil
self?.removeSleepTimer()
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
private func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Int?) {
// Not a chapter sleep timer
let hadToCancelChapterSleepTimer = decideIfChapterSleepTimerNeedsToBeCanceled(time: time)
guard !hadToCancelChapterSleepTimer else { return }
guard PlayerHandler.sleepTimerChapterStopTime == nil else { return }
// Verify sleep timer is set
guard self.sleepTimeToken != nil else { return }
// Update the sleep timer
if let secondsRemaining = secondsRemaining {
let newSleepTimerPosition = time + Double(secondsRemaining)
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
private func decideIfChapterSleepTimerNeedsToBeCanceled(time: Double) -> Bool {
if let chapterSleepTime = PlayerHandler.sleepTimerChapterStopTime {
let sleepIsBeforeCurrentTime = Double(chapterSleepTime) <= time
if sleepIsBeforeCurrentTime {
PlayerHandler.sleepTimerChapterStopTime = nil
self.removeSleepTimer()
return true
}
}
return false
}
public func increaseSleepTime(extraTimeInSeconds: Double) {
if let sleepTime = PlayerHandler.remainingSleepTime, let currentTime = getCurrentTime() {
let newSleepTimerPosition = currentTime + Double(sleepTime) + extraTimeInSeconds
if newSleepTimerPosition > currentTime {
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
}
public func decreaseSleepTime(removeTimeInSeconds: Double) {
if let sleepTime = PlayerHandler.remainingSleepTime, let currentTime = getCurrentTime() {
let newSleepTimerPosition = currentTime + Double(sleepTime) - removeTimeInSeconds
guard newSleepTimerPosition > currentTime else { return }
if newSleepTimerPosition > currentTime {
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
}
public func removeSleepTimer() {
self.sleepTimeStopAt = nil
if let token = sleepTimeToken {
self.audioPlayer.removeTimeObserver(token)
sleepTimeToken = nil
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self)
}
public func getCurrentTime() -> Double? { public func getCurrentTime() -> Double? {
guard let playbackSession = self.getPlaybackSession() else { return nil } guard let playbackSession = self.getPlaybackSession() else { return nil }
let currentTrackTime = self.audioPlayer.currentTime().seconds let currentTrackTime = self.audioPlayer.currentTime().seconds
@ -660,59 +570,59 @@ class AudioPlayer: NSObject {
let deviceSettings = Database.shared.getDeviceSettings() let deviceSettings = Database.shared.getDeviceSettings()
commandCenter.playCommand.isEnabled = true commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { [unowned self] event in commandCenter.playCommand.addTarget { [weak self] event in
play(allowSeekBack: true) self?.play(allowSeekBack: true)
return .success return .success
} }
commandCenter.pauseCommand.isEnabled = true commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { [unowned self] event in commandCenter.pauseCommand.addTarget { [weak self] event in
pause() self?.pause()
return .success return .success
} }
commandCenter.skipForwardCommand.isEnabled = true commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpForwardTime)] commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpForwardTime)]
commandCenter.skipForwardCommand.addTarget { [unowned self] event in commandCenter.skipForwardCommand.addTarget { [weak self] event in
guard let command = event.command as? MPSkipIntervalCommand else { guard let command = event.command as? MPSkipIntervalCommand else {
return .noSuchContent return .noSuchContent
} }
guard let currentTime = self.getCurrentTime() else { guard let currentTime = self?.getCurrentTime() else {
return .commandFailed return .commandFailed
} }
seek(currentTime + command.preferredIntervals[0].doubleValue, from: "remote") self?.seek(currentTime + command.preferredIntervals[0].doubleValue, from: "remote")
return .success return .success
} }
commandCenter.skipBackwardCommand.isEnabled = true commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpBackwardsTime)] commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpBackwardsTime)]
commandCenter.skipBackwardCommand.addTarget { [unowned self] event in commandCenter.skipBackwardCommand.addTarget { [weak self] event in
guard let command = event.command as? MPSkipIntervalCommand else { guard let command = event.command as? MPSkipIntervalCommand else {
return .noSuchContent return .noSuchContent
} }
guard let currentTime = self.getCurrentTime() else { guard let currentTime = self?.getCurrentTime() else {
return .commandFailed return .commandFailed
} }
seek(currentTime - command.preferredIntervals[0].doubleValue, from: "remote") self?.seek(currentTime - command.preferredIntervals[0].doubleValue, from: "remote")
return .success return .success
} }
commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { event in commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
guard let event = event as? MPChangePlaybackPositionCommandEvent else { guard let event = event as? MPChangePlaybackPositionCommandEvent else {
return .noSuchContent return .noSuchContent
} }
self.seek(event.positionTime, from: "remote") self?.seek(event.positionTime, from: "remote")
return .success return .success
} }
commandCenter.changePlaybackRateCommand.isEnabled = true commandCenter.changePlaybackRateCommand.isEnabled = true
commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2] commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2]
commandCenter.changePlaybackRateCommand.addTarget { event in commandCenter.changePlaybackRateCommand.addTarget { [weak self] event in
guard let event = event as? MPChangePlaybackRateCommandEvent else { guard let event = event as? MPChangePlaybackRateCommandEvent else {
return .noSuchContent return .noSuchContent
} }
self.setPlaybackRate(event.playbackRate) self?.setPlaybackRate(event.playbackRate)
return .success return .success
} }
} }

View file

@ -0,0 +1,156 @@
//
// AudioPlayerSleepTimer.swift
// App
//
// Created by Ron Heft on 9/2/22.
//
import Foundation
import AVFoundation
extension AudioPlayer {
// MARK: - Public API
public func isSleepTimerSet() -> Bool {
return self.isCountdownSleepTimerSet() || self.isChapterSleepTimerSet()
}
public func getSleepTimeRemaining() -> Double? {
guard let currentTime = self.getCurrentTime() else { return nil }
// Return the player time until sleep
var sleepTimeRemaining: Double? = nil
if let chapterStopAt = self.sleepTimeChapterStopAt {
sleepTimeRemaining = (chapterStopAt - currentTime) / Double(self.rate > 0 ? self.rate : 1.0)
} else if self.isCountdownSleepTimerSet() {
sleepTimeRemaining = self.sleepTimeRemaining
}
return sleepTimeRemaining
}
public func setSleepTimer(secondsUntilSleep: Double) {
NSLog("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds")
self.removeSleepTimer()
self.sleepTimeRemaining = secondsUntilSleep
DispatchQueue.runOnMainQueue {
self.sleepTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
if self?.isPlaying() ?? false {
self?.decrementSleepTimerIfRunning()
}
}
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
public func setChapterSleepTimer(stopAt: Double) {
NSLog("SLEEP TIMER: Scheduling for chapter end \(stopAt)")
self.removeSleepTimer()
// Schedule the observation time
self.sleepTimeChapterStopAt = stopAt
// Get the current track
guard let playbackSession = self.getPlaybackSession() else { return }
let currentTrack = playbackSession.audioTracks[currentTrackIndex]
// Set values
guard let trackStartTime = currentTrack.startOffset else { return }
guard let trackEndTime = currentTrack.endOffset else { return }
// Verify the stop is during the current audio track
guard trackEndTime >= stopAt else { return }
// Schedule the observation time
let trackBasedStopTime = stopAt - trackStartTime
let sleepTime = CMTime(seconds: trackBasedStopTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
var times = [NSValue]()
times.append(NSValue(time: sleepTime))
self.sleepTimeChapterToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in
self?.handleSleepEnd()
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
public func increaseSleepTime(extraTimeInSeconds: Double) {
self.removeChapterSleepTimer()
guard let sleepTimeRemaining = self.sleepTimeRemaining else { return }
self.sleepTimeRemaining = sleepTimeRemaining + extraTimeInSeconds
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
public func decreaseSleepTime(removeTimeInSeconds: Double) {
self.removeChapterSleepTimer()
guard let sleepTimeRemaining = self.sleepTimeRemaining else { return }
self.sleepTimeRemaining = sleepTimeRemaining - removeTimeInSeconds
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
public func removeSleepTimer() {
self.sleepTimer?.invalidate()
self.sleepTimer = nil
self.removeChapterSleepTimer()
self.sleepTimeRemaining = nil
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self)
}
// MARK: - Internal helpers
internal func decrementSleepTimerIfRunning() {
if var sleepTimeRemaining = self.sleepTimeRemaining {
sleepTimeRemaining -= 1
self.sleepTimeRemaining = sleepTimeRemaining
// Handle the sleep if the timer has expired
if sleepTimeRemaining <= 0 {
self.handleSleepEnd()
}
}
}
private func handleSleepEnd() {
NSLog("SLEEP TIMER: Pausing audio")
self.pause()
self.removeSleepTimer()
}
private func removeChapterSleepTimer() {
if let token = self.sleepTimeChapterToken {
self.audioPlayer.removeTimeObserver(token)
}
self.sleepTimeChapterToken = nil
self.sleepTimeChapterStopAt = nil
}
internal func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool {
if let chapterStopAt = self.sleepTimeChapterStopAt {
return chapterStopAt <= time
}
return false
}
internal func isCountdownSleepTimerSet() -> Bool {
return self.sleepTimeRemaining != nil
}
internal func isChapterSleepTimerSet() -> Bool {
return self.sleepTimeChapterStopAt != nil
}
}

View file

@ -11,8 +11,6 @@ import RealmSwift
class PlayerHandler { class PlayerHandler {
private static var player: AudioPlayer? private static var player: AudioPlayer?
public static var sleepTimerChapterStopTime: Int? = nil
public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) { public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) {
guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return } guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return }
@ -59,34 +57,6 @@ class PlayerHandler {
} }
} }
public static var remainingSleepTime: Int? {
get {
guard let player = player else { return nil }
guard let currentTime = player.getCurrentTime() else { return nil }
// Return the player time until sleep
var timeUntilSleep: Double? = nil
if let sleepTimerChapterStopTime = sleepTimerChapterStopTime {
timeUntilSleep = Double(sleepTimerChapterStopTime) - currentTime
} else if let stopAt = player.getSleepStopAt() {
timeUntilSleep = stopAt - currentTime
}
// Scale the time until sleep based on the playback rate
if let timeUntilSleep = timeUntilSleep {
// Consider paused as playing at 1x
let rate = Double(player.rate > 0 ? player.rate : 1)
let timeUntilSleepScaled = timeUntilSleep / rate
guard timeUntilSleepScaled.isNaN == false else { return nil }
return Int(timeUntilSleepScaled.rounded())
} else {
return nil
}
}
}
public static func getCurrentTime() -> Double? { public static func getCurrentTime() -> Double? {
self.player?.getCurrentTime() self.player?.getCurrentTime()
} }
@ -95,31 +65,27 @@ class PlayerHandler {
self.player?.setPlaybackRate(speed) self.player?.setPlaybackRate(speed)
} }
public static func getSleepTimeRemaining() -> Double? {
return self.player?.getSleepTimeRemaining()
}
public static func setSleepTime(secondsUntilSleep: Double) { public static func setSleepTime(secondsUntilSleep: Double) {
guard let player = player else { return } self.player?.setSleepTimer(secondsUntilSleep: secondsUntilSleep)
guard let currentTime = player.getCurrentTime() else { return }
let stopAt = secondsUntilSleep + currentTime
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true)
} }
public static func setChapterSleepTime(stopAt: Double) { public static func setChapterSleepTime(stopAt: Double) {
guard let player = player else { return } self.player?.setChapterSleepTimer(stopAt: stopAt)
self.sleepTimerChapterStopTime = Int(stopAt)
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false)
} }
public static func increaseSleepTime(increaseSeconds: Double) { public static func increaseSleepTime(increaseSeconds: Double) {
self.sleepTimerChapterStopTime = nil
self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds) self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds)
} }
public static func decreaseSleepTime(decreaseSeconds: Double) { public static func decreaseSleepTime(decreaseSeconds: Double) {
self.sleepTimerChapterStopTime = nil
self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds) self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds)
} }
public static func cancelSleepTime() { public static func cancelSleepTime() {
PlayerHandler.sleepTimerChapterStopTime = nil
self.player?.removeSleepTimer() self.player?.removeSleepTimer()
} }
@ -136,6 +102,7 @@ class PlayerHandler {
public static func seekForward(amount: Double) { public static func seekForward(amount: Double) {
guard let player = player else { return } guard let player = player else { return }
guard player.isInitialized() else { return }
guard let currentTime = player.getCurrentTime() else { return } guard let currentTime = player.getCurrentTime() else { return }
let destinationTime = currentTime + amount let destinationTime = currentTime + amount
@ -144,6 +111,7 @@ class PlayerHandler {
public static func seekBackward(amount: Double) { public static func seekBackward(amount: Double) {
guard let player = player else { return } guard let player = player else { return }
guard player.isInitialized() else { return }
guard let currentTime = player.getCurrentTime() else { return } guard let currentTime = player.getCurrentTime() else { return }
let destinationTime = currentTime - amount let destinationTime = currentTime - amount
@ -151,7 +119,10 @@ class PlayerHandler {
} }
public static func seek(amount: Double) { public static func seek(amount: Double) {
player?.seek(amount, from: "handler") guard let player = player else { return }
guard player.isInitialized() else { return }
player.seek(amount, from: "handler")
} }
public static func getMetdata() -> [String: Any]? { public static func getMetdata() -> [String: Any]? {