mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-29 14:28:34 +02:00
Sleep timer using native time observer
This commit is contained in:
parent
8952cbfd20
commit
d57fe44bcc
4 changed files with 264 additions and 172 deletions
|
@ -166,45 +166,47 @@ 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 = Int(timeString) else { return call.resolve([ "success": false ]) }
|
guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
|
||||||
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
|
guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
|
||||||
|
|
||||||
PlayerHandler.remainingSleepTime = currentSleepTime - (time / 1000)
|
let seconds = time/1000
|
||||||
|
PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 = Int(timeString) else { return call.resolve([ "success": false ]) }
|
guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
|
||||||
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
|
guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
|
||||||
|
|
||||||
PlayerHandler.remainingSleepTime = currentSleepTime + (time / 1000)
|
let seconds = time/1000
|
||||||
|
PlayerHandler.increaseSleepTime(increaseSeconds: seconds)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 = Int(timeString) else { return call.resolve([ "success": false ]) }
|
||||||
let timeSeconds = time / 1000
|
let isChapterTime = call.getBool("isChapterTime", false)
|
||||||
|
|
||||||
NSLog("chapter time: \(call.getBool("isChapterTime", false))")
|
let seconds = time / 1000
|
||||||
|
|
||||||
if call.getBool("isChapterTime", false) {
|
NSLog("chapter time: \(isChapterTime)")
|
||||||
let timeToPause = timeSeconds - Int(PlayerHandler.getCurrentTime() ?? 0)
|
if isChapterTime {
|
||||||
if timeToPause < 0 { return call.resolve([ "success": false ]) }
|
PlayerHandler.setChapterSleepTime(stopAt: Double(seconds))
|
||||||
|
|
||||||
PlayerHandler.sleepTimerChapterStopTime = timeSeconds
|
|
||||||
PlayerHandler.remainingSleepTime = timeToPause
|
|
||||||
return call.resolve([ "success": true ])
|
return call.resolve([ "success": true ])
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerHandler.sleepTimerChapterStopTime = nil
|
PlayerHandler.setSleepTime(secondsUntilSleep: Double(seconds))
|
||||||
PlayerHandler.remainingSleepTime = timeSeconds
|
|
||||||
call.resolve([ "success": true ])
|
call.resolve([ "success": true ])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func cancelSleepTimer(_ call: CAPPluginCall) {
|
@objc func cancelSleepTimer(_ call: CAPPluginCall) {
|
||||||
PlayerHandler.remainingSleepTime = nil
|
PlayerHandler.cancelSleepTime()
|
||||||
PlayerHandler.sleepTimerChapterStopTime = nil
|
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.remainingSleepTime
|
||||||
|
|
|
@ -38,6 +38,9 @@ class AudioPlayer: NSObject {
|
||||||
private var queueObserver:NSKeyValueObservation?
|
private var queueObserver:NSKeyValueObservation?
|
||||||
private var queueItemStatusObserver:NSKeyValueObservation?
|
private var queueItemStatusObserver:NSKeyValueObservation?
|
||||||
|
|
||||||
|
private var sleepTimeStopAt: Double?
|
||||||
|
private var sleepTimeToken: Any?
|
||||||
|
|
||||||
private var currentTrackIndex = 0
|
private var currentTrackIndex = 0
|
||||||
private var allPlayerItems:[AVPlayerItem] = []
|
private var allPlayerItems:[AVPlayerItem] = []
|
||||||
|
|
||||||
|
@ -85,6 +88,7 @@ class AudioPlayer: NSObject {
|
||||||
NSLog("Audioplayer ready")
|
NSLog("Audioplayer ready")
|
||||||
}
|
}
|
||||||
deinit {
|
deinit {
|
||||||
|
self.removeSleepTimer()
|
||||||
self.removeTimeObserver()
|
self.removeTimeObserver()
|
||||||
self.queueObserver?.invalidate()
|
self.queueObserver?.invalidate()
|
||||||
self.queueItemStatusObserver?.invalidate()
|
self.queueItemStatusObserver?.invalidate()
|
||||||
|
@ -129,9 +133,18 @@ class AudioPlayer: NSObject {
|
||||||
|
|
||||||
private func setupTimeObserver() {
|
private func setupTimeObserver() {
|
||||||
let timeScale = CMTimeScale(NSEC_PER_SEC)
|
let timeScale = CMTimeScale(NSEC_PER_SEC)
|
||||||
let time = CMTime(seconds: 1, preferredTimescale: timeScale)
|
// Observe multiple times per seconds, as rate will be different depending on playback speed
|
||||||
self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] currentTime in
|
let time = CMTime(seconds: 0.25, preferredTimescale: timeScale)
|
||||||
NSLog("currentTime: \(currentTime)")
|
self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { time in
|
||||||
|
Task {
|
||||||
|
// Let the player update the current playback positions
|
||||||
|
await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false)
|
||||||
|
|
||||||
|
// Update the sleep time, if set
|
||||||
|
if self.sleepTimeStopAt != nil {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +223,11 @@ class AudioPlayer: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
||||||
|
|
||||||
|
Task {
|
||||||
|
let isPlaying = self.status > 0
|
||||||
|
await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: isPlaying, isStopping: false)
|
||||||
|
}
|
||||||
|
|
||||||
self.audioPlayer.play()
|
self.audioPlayer.play()
|
||||||
self.status = 1
|
self.status = 1
|
||||||
|
@ -224,6 +242,10 @@ class AudioPlayer: NSObject {
|
||||||
self.status = 0
|
self.status = 0
|
||||||
self.rate = 0.0
|
self.rate = 0.0
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: true, isStopping: true)
|
||||||
|
}
|
||||||
|
|
||||||
updateNowPlaying()
|
updateNowPlaying()
|
||||||
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
||||||
}
|
}
|
||||||
|
@ -242,6 +264,8 @@ class AudioPlayer: NSObject {
|
||||||
let trackEnd = ctso + currentTrack.duration
|
let trackEnd = ctso + currentTrack.duration
|
||||||
NSLog("Seek current track END = \(trackEnd)")
|
NSLog("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 to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
||||||
|
@ -269,15 +293,21 @@ class AudioPlayer: NSObject {
|
||||||
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)) { completed in
|
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
||||||
if !completed {
|
if !completed {
|
||||||
NSLog("WARNING: seeking not completed (to \(seekTime)")
|
NSLog("WARNING: seeking not completed (to \(seekTime)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if continuePlaying {
|
if continuePlaying {
|
||||||
self.play()
|
self?.play()
|
||||||
|
}
|
||||||
|
self?.updateNowPlaying()
|
||||||
|
|
||||||
|
// If we have an active sleep timer, reschedule based on seek, since seek is fuzzy
|
||||||
|
// Theis needs to occur after play() to capture the correct rate
|
||||||
|
if let currentTime = self?.getCurrentTime() {
|
||||||
|
self?.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
|
||||||
}
|
}
|
||||||
self.updateNowPlaying()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -291,8 +321,103 @@ class AudioPlayer: NSObject {
|
||||||
self.tmpRate = rate
|
self.tmpRate = rate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture remaining sleep time before changing the rate
|
||||||
|
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
|
||||||
|
|
||||||
self.rate = rate
|
self.rate = rate
|
||||||
self.updateNowPlaying()
|
self.updateNowPlaying()
|
||||||
|
|
||||||
|
// If we have an active sleep timer, reschedule based on rate
|
||||||
|
self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
let currentTime = getCurrentTime()
|
||||||
|
|
||||||
|
// 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: .main) { [weak self] in
|
||||||
|
NSLog("SLEEP TIMER: Pausing audio")
|
||||||
|
self?.pause()
|
||||||
|
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
|
||||||
|
guard PlayerHandler.sleepTimerChapterStopTime == nil else { return }
|
||||||
|
|
||||||
|
// Update the sleep timer
|
||||||
|
if let secondsRemaining = secondsRemaining {
|
||||||
|
let newSleepTimerPosition = time + Double(secondsRemaining)
|
||||||
|
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
PlayerHandler.sleepTimerChapterStopTime = nil
|
||||||
|
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 {
|
||||||
|
|
|
@ -10,85 +10,8 @@ import RealmSwift
|
||||||
|
|
||||||
class PlayerHandler {
|
class PlayerHandler {
|
||||||
private static var player: AudioPlayer?
|
private static var player: AudioPlayer?
|
||||||
private static var playingTimer: Timer?
|
|
||||||
private static var pausedTimer: Timer?
|
|
||||||
private static var lastSyncTime: Double = 0.0
|
|
||||||
|
|
||||||
public static var sleepTimerChapterStopTime: Int? = nil
|
public static var sleepTimerChapterStopTime: Int? = nil
|
||||||
private static var _remainingSleepTime: Int? = nil
|
|
||||||
public static var remainingSleepTime: Int? {
|
|
||||||
get {
|
|
||||||
return _remainingSleepTime
|
|
||||||
}
|
|
||||||
set(time) {
|
|
||||||
if time != nil && time! < 0 {
|
|
||||||
_remainingSleepTime = nil
|
|
||||||
} else {
|
|
||||||
_remainingSleepTime = time
|
|
||||||
}
|
|
||||||
|
|
||||||
if _remainingSleepTime == nil {
|
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: _remainingSleepTime)
|
|
||||||
} else {
|
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: _remainingSleepTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private static var listeningTimePassedSinceLastSync: Double = 0.0
|
|
||||||
|
|
||||||
public static var paused: Bool {
|
|
||||||
get {
|
|
||||||
guard let player = player else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return player.rate == 0.0
|
|
||||||
}
|
|
||||||
set(paused) {
|
|
||||||
if paused {
|
|
||||||
self.player?.pause()
|
|
||||||
} else {
|
|
||||||
self.player?.play()
|
|
||||||
self.pausedTimer?.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func startTickTimer() {
|
|
||||||
DispatchQueue.runOnMainQueue {
|
|
||||||
NSLog("Starting the tick timer")
|
|
||||||
playingTimer?.invalidate()
|
|
||||||
pausedTimer?.invalidate()
|
|
||||||
playingTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
|
||||||
self.tick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func stopTickTimer() {
|
|
||||||
NSLog("Stopping the tick timer")
|
|
||||||
playingTimer?.invalidate()
|
|
||||||
pausedTimer?.invalidate()
|
|
||||||
playingTimer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func startPausedTimer() {
|
|
||||||
guard self.paused else { return }
|
|
||||||
self.pausedTimer?.invalidate()
|
|
||||||
self.pausedTimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(syncServerProgressDuringPause), userInfo: nil, repeats: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func cleanupOldSessions(currentSessionId: String?) {
|
|
||||||
let realm = try! Realm()
|
|
||||||
let oldSessions = realm.objects(PlaybackSession.self) .where({ $0.isActiveSession == true })
|
|
||||||
try! realm.write {
|
|
||||||
for s in oldSessions {
|
|
||||||
if s.id != currentSessionId {
|
|
||||||
s.isActiveSession = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
||||||
|
@ -99,26 +22,21 @@ class PlayerHandler {
|
||||||
player = nil
|
player = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup old sessions
|
// Cleanup and sync old sessions
|
||||||
cleanupOldSessions(currentSessionId: sessionId)
|
cleanupOldSessions(currentSessionId: sessionId)
|
||||||
|
Task { await PlayerProgress.shared.syncToServer() }
|
||||||
|
|
||||||
// Set now playing info
|
// Set now playing info
|
||||||
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
|
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
|
||||||
|
|
||||||
// Create the audio player
|
// Create the audio player
|
||||||
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
|
|
||||||
startTickTimer()
|
|
||||||
startPausedTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func stopPlayback() {
|
public static func stopPlayback() {
|
||||||
// Pause playback first, so we can sync our current progress
|
// Pause playback first, so we can sync our current progress
|
||||||
player?.pause()
|
player?.pause()
|
||||||
|
|
||||||
// Stop updating progress before we destory the player, so we don't receive bad data
|
|
||||||
stopTickTimer()
|
|
||||||
|
|
||||||
player?.destroy()
|
player?.destroy()
|
||||||
player = nil
|
player = nil
|
||||||
|
|
||||||
|
@ -127,6 +45,41 @@ class PlayerHandler {
|
||||||
NowPlayingInfo.shared.reset()
|
NowPlayingInfo.shared.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var paused: Bool {
|
||||||
|
get {
|
||||||
|
guard let player = player else { return true }
|
||||||
|
return player.rate == 0.0
|
||||||
|
}
|
||||||
|
set(paused) {
|
||||||
|
if paused {
|
||||||
|
self.player?.pause()
|
||||||
|
} else {
|
||||||
|
self.player?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var remainingSleepTime: Int? {
|
||||||
|
get {
|
||||||
|
guard let player = player else { return nil }
|
||||||
|
|
||||||
|
// Consider paused as playing at 1x
|
||||||
|
let rate = Double(player.rate > 0 ? player.rate : 1)
|
||||||
|
|
||||||
|
if let sleepTimerChapterStopTime = sleepTimerChapterStopTime {
|
||||||
|
let timeUntilChapterEnd = Double(sleepTimerChapterStopTime) - player.getCurrentTime()
|
||||||
|
let timeUntilChapterEndScaled = timeUntilChapterEnd / rate
|
||||||
|
return Int(timeUntilChapterEndScaled.rounded())
|
||||||
|
} else if let stopAt = player.getSleepStopAt() {
|
||||||
|
let timeUntilSleep = stopAt - player.getCurrentTime()
|
||||||
|
let timeUntilSleepScaled = timeUntilSleep / rate
|
||||||
|
return Int(timeUntilSleepScaled.rounded())
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func getCurrentTime() -> Double? {
|
public static func getCurrentTime() -> Double? {
|
||||||
self.player?.getCurrentTime()
|
self.player?.getCurrentTime()
|
||||||
}
|
}
|
||||||
|
@ -135,6 +88,30 @@ class PlayerHandler {
|
||||||
self.player?.setPlaybackRate(speed)
|
self.player?.setPlaybackRate(speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func setSleepTime(secondsUntilSleep: Double) {
|
||||||
|
guard let player = player else { return }
|
||||||
|
let stopAt = secondsUntilSleep + player.getCurrentTime()
|
||||||
|
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func setChapterSleepTime(stopAt: Double) {
|
||||||
|
guard let player = player else { return }
|
||||||
|
self.sleepTimerChapterStopTime = Int(stopAt)
|
||||||
|
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func increaseSleepTime(increaseSeconds: Double) {
|
||||||
|
self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func decreaseSleepTime(decreaseSeconds: Double) {
|
||||||
|
self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func cancelSleepTime() {
|
||||||
|
self.player?.removeSleepTimer()
|
||||||
|
}
|
||||||
|
|
||||||
public static func getPlayMethod() -> Int? {
|
public static func getPlayMethod() -> Int? {
|
||||||
self.player?.getPlayMethod()
|
self.player?.getPlayMethod()
|
||||||
}
|
}
|
||||||
|
@ -142,8 +119,8 @@ class PlayerHandler {
|
||||||
public static func getPlaybackSession() -> PlaybackSession? {
|
public static func getPlaybackSession() -> PlaybackSession? {
|
||||||
guard let player = player else { return nil }
|
guard let player = player else { return nil }
|
||||||
guard player.isInitialized() else { return nil }
|
guard player.isInitialized() else { return nil }
|
||||||
guard let session = Database.shared.getPlaybackSession(id: player.getPlaybackSessionId()) else { return nil }
|
|
||||||
return session
|
return Database.shared.getPlaybackSession(id: player.getPlaybackSessionId())
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func seekForward(amount: Double) {
|
public static func seekForward(amount: Double) {
|
||||||
|
@ -168,10 +145,6 @@ class PlayerHandler {
|
||||||
guard let player = player else { return nil }
|
guard let player = player else { return nil }
|
||||||
guard player.isInitialized() else { return nil }
|
guard player.isInitialized() else { return nil }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
syncPlayerProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"duration": player.getDuration(),
|
"duration": player.getDuration(),
|
||||||
"currentTime": player.getCurrentTime(),
|
"currentTime": player.getCurrentTime(),
|
||||||
|
@ -180,65 +153,18 @@ class PlayerHandler {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func tick() {
|
// MARK: - Helper logic
|
||||||
if !paused {
|
|
||||||
listeningTimePassedSinceLastSync += 1
|
private static func cleanupOldSessions(currentSessionId: String?) {
|
||||||
|
let realm = try! Realm()
|
||||||
if remainingSleepTime != nil {
|
let oldSessions = realm.objects(PlaybackSession.self) .where({ $0.isActiveSession == true })
|
||||||
if sleepTimerChapterStopTime != nil {
|
try! realm.write {
|
||||||
let timeUntilChapterEnd = Double(sleepTimerChapterStopTime ?? 0) - (getCurrentTime() ?? 0)
|
for s in oldSessions {
|
||||||
if timeUntilChapterEnd <= 0 {
|
if s.id != currentSessionId {
|
||||||
paused = true
|
s.isActiveSession = false
|
||||||
remainingSleepTime = nil
|
|
||||||
} else {
|
|
||||||
remainingSleepTime = Int(timeUntilChapterEnd.rounded())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if remainingSleepTime! <= 0 {
|
|
||||||
paused = true
|
|
||||||
}
|
|
||||||
remainingSleepTime! -= 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if listeningTimePassedSinceLastSync >= 5 {
|
|
||||||
syncPlayerProgress()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func syncPlayerProgress() {
|
|
||||||
guard let player = player else { return }
|
|
||||||
guard player.isInitialized() else { return }
|
|
||||||
guard let session = getPlaybackSession() else { return }
|
|
||||||
|
|
||||||
NSLog("Syncing player progress")
|
|
||||||
|
|
||||||
// Get current time
|
|
||||||
let playerCurrentTime = player.getCurrentTime()
|
|
||||||
|
|
||||||
// Prevent multiple sync requests
|
|
||||||
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
|
|
||||||
if (lastSyncTime > 0 && timeSinceLastSync < 1) {
|
|
||||||
NSLog("syncProgress last sync time was < 1 second so not syncing")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a sync if we got junk data from the player (occurs when exiting out of memory
|
|
||||||
guard !playerCurrentTime.isNaN else { return }
|
|
||||||
|
|
||||||
lastSyncTime = Date().timeIntervalSince1970 // seconds
|
|
||||||
|
|
||||||
session.update {
|
|
||||||
session.currentTime = playerCurrentTime
|
|
||||||
session.timeListening += listeningTimePassedSinceLastSync
|
|
||||||
session.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
||||||
}
|
|
||||||
listeningTimePassedSinceLastSync = 0
|
|
||||||
|
|
||||||
// Persist items in the database and sync to the server
|
|
||||||
if session.isLocal { Task { await PlayerProgress.shared.syncFromPlayer() } }
|
|
||||||
Task { await PlayerProgress.shared.syncToServer() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public static func syncServerProgressDuringPause() {
|
@objc public static func syncServerProgressDuringPause() {
|
||||||
|
|
|
@ -13,11 +13,20 @@ class PlayerProgress {
|
||||||
|
|
||||||
public static let shared = PlayerProgress()
|
public static let shared = PlayerProgress()
|
||||||
|
|
||||||
|
private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
public func syncFromPlayer() async {
|
|
||||||
|
// MARK: - SYNC HOOKS
|
||||||
|
|
||||||
|
public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async {
|
||||||
let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer")
|
let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer")
|
||||||
|
let session = await updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress)
|
||||||
updateLocalMediaProgressFromLocalSession()
|
updateLocalMediaProgressFromLocalSession()
|
||||||
|
if let session = session {
|
||||||
|
await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
|
||||||
|
}
|
||||||
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,8 +42,26 @@ class PlayerProgress {
|
||||||
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLocalSessionFromPlayer() async {
|
|
||||||
|
// MARK: - SYNC LOGIC
|
||||||
|
|
||||||
|
private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) async -> PlaybackSession? {
|
||||||
|
guard let session = PlayerHandler.getPlaybackSession() else { return nil }
|
||||||
|
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
let lastUpdate = session.updatedAt ?? now
|
||||||
|
let timeSinceLastUpdate = now - lastUpdate
|
||||||
|
|
||||||
|
session.update {
|
||||||
|
session.currentTime = currentTime
|
||||||
|
session.updatedAt = now
|
||||||
|
|
||||||
|
if includesPlayProgress {
|
||||||
|
session.timeListening += timeSinceLastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.freeze()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLocalMediaProgressFromLocalSession() {
|
private func updateLocalMediaProgressFromLocalSession() {
|
||||||
|
@ -65,7 +92,19 @@ class PlayerProgress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateServerSessionFromLocalSession(_ session: PlaybackSession) async {
|
private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async {
|
||||||
|
// If required, rate limit requests based on session last update
|
||||||
|
if rateLimitSync {
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
let lastUpdate = session.updatedAt ?? now
|
||||||
|
let timeSinceLastSync = now - lastUpdate
|
||||||
|
let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000
|
||||||
|
guard timeSinceLastSync > timeBetweenSessionSync else {
|
||||||
|
// Skipping sync since last occurred within session sync time
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSLog("Sending sessionId(\(session.id)) to server")
|
NSLog("Sending sessionId(\(session.id)) to server")
|
||||||
|
|
||||||
var success = false
|
var success = false
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue