Fix crashes related to Realm threading

This commit is contained in:
ronaldheft
2022-08-16 12:32:22 -04:00
parent 8ce0d9ce56
commit b0905d0270
8 changed files with 236 additions and 75 deletions
@@ -0,0 +1,41 @@
//
// ActivePlaybackSession.swift
// App
//
// Created by Ron Heft on 8/16/22.
//
import Foundation
import RealmSwift
class ActivePlaybackSession {
static let shared = ActivePlaybackSession()
private let queue = DispatchQueue(label: "ABSActivePlaybackSession")
private var _session: PlaybackSession?
private init() {
// Singleton
}
func startSession(_ session: ThreadSafeReference<PlaybackSession>) {
queue.sync {
_session = try? Realm().resolve(session)
}
}
// This is a funky method, but it ensures the accessing thread gets a live reference to session properly resolved
func get() -> PlaybackSession? {
var activeSession: ThreadSafeReference<PlaybackSession>?
queue.sync {
let realm = try! Realm()
guard let session = _session else { return }
r
activeSession = ThreadSafeReference(to: session)
}
guard let activeSession = activeSession else { return nil }
return try? Realm().resolve(activeSession)
}
}
+23 -11
View File
@@ -32,7 +32,7 @@ class AudioPlayer: NSObject {
private var initialPlaybackRate: Float
private var audioPlayer: AVQueuePlayer
private var playbackSession: PlaybackSession
private var sessionId: String
private var queueObserver:NSKeyValueObservation?
private var queueItemStatusObserver:NSKeyValueObservation?
@@ -41,12 +41,12 @@ class AudioPlayer: NSObject {
private var allPlayerItems:[AVPlayerItem] = []
// MARK: - Constructor
init(playbackSession: PlaybackSession, playWhenReady: Bool = false, playbackRate: Float = 1) {
init(sessionId: String, playWhenReady: Bool = false, playbackRate: Float = 1) {
self.playWhenReady = playWhenReady
self.initialPlaybackRate = playbackRate
self.audioPlayer = AVQueuePlayer()
self.audioPlayer.automaticallyWaitsToMinimizeStalling = false
self.playbackSession = playbackSession
self.sessionId = sessionId
self.status = -1
self.rate = 0.0
self.tmpRate = playbackRate
@@ -56,6 +56,8 @@ class AudioPlayer: NSObject {
initAudioSession()
setupRemoteTransportControls()
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
// 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)
@@ -106,6 +108,7 @@ class AudioPlayer: NSObject {
}
func getItemIndexForTime(time:Double) -> Int {
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
for index in 0..<self.allPlayerItems.count {
let startOffset = playbackSession.audioTracks[index].startOffset ?? 0.0
let duration = playbackSession.audioTracks[index].duration
@@ -132,6 +135,7 @@ class AudioPlayer: NSObject {
func setupQueueItemStatusObserver() {
self.queueItemStatusObserver?.invalidate()
self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
if (playerItem.status == .readyToPlay) {
NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
self.updateNowPlaying()
@@ -139,11 +143,11 @@ class AudioPlayer: NSObject {
let firstReady = self.status < 0
self.status = 0
if self.playWhenReady {
self.seek(self.playbackSession.currentTime, from: "queueItemStatusObserver")
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
self.playWhenReady = false
self.play()
} else if (firstReady) { // Only seek on first readyToPlay
self.seek(self.playbackSession.currentTime, from: "queueItemStatusObserver")
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
}
} else if (playerItem.status == .failed) {
NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
@@ -205,7 +209,9 @@ class AudioPlayer: NSObject {
NSLog("Seek to \(to) from \(from)")
let currentTrack = self.playbackSession.audioTracks[self.currentTrackIndex]
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
let ctso = currentTrack.startOffset ?? 0.0
let trackEnd = ctso + currentTrack.duration
NSLog("Seek current track END = \(trackEnd)")
@@ -218,7 +224,9 @@ class AudioPlayer: NSObject {
if (self.currentTrackIndex != indexOfSeek) {
self.currentTrackIndex = indexOfSeek
self.playbackSession.currentTime = to
playbackSession.update {
playbackSession.currentTime = to
}
self.playWhenReady = continuePlaying // Only playWhenReady if already playing
self.status = -1
@@ -232,7 +240,7 @@ class AudioPlayer: NSObject {
setupQueueItemStatusObserver()
} else {
NSLog("Seeking in current item \(to)")
let currentTrackStartOffset = self.playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let seekTime = to - currentTrackStartOffset
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { completed in
@@ -262,6 +270,7 @@ class AudioPlayer: NSObject {
}
public func getCurrentTime() -> Double {
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
let currentTrackTime = self.audioPlayer.currentTime().seconds
let audioTrack = playbackSession.audioTracks[currentTrackIndex]
let startOffset = audioTrack.startOffset ?? 0.0
@@ -269,17 +278,20 @@ class AudioPlayer: NSObject {
}
public func getPlayMethod() -> Int {
return self.playbackSession.playMethod
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
return playbackSession.playMethod
}
public func getPlaybackSession() -> PlaybackSession {
return self.playbackSession
public func getPlaybackSessionId() -> String {
return self.sessionId
}
public func getDuration() -> Double {
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
return playbackSession.duration
}
// MARK: - Private
private func createAsset(itemId:String, track:AudioTrack) -> AVAsset {
let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)!
if (playbackSession.playMethod == PlayMethod.directplay.rawValue) {
// The only reason this is separate is because the filename needs to be encoded
let filename = track.metadata?.filename ?? ""
+15 -11
View File
@@ -6,10 +6,10 @@
//
import Foundation
import RealmSwift
class PlayerHandler {
private static var player: AudioPlayer?
private static var session: PlaybackSession?
private static var timer: Timer?
private static var lastSyncTime: Double = 0.0
@@ -68,7 +68,8 @@ class PlayerHandler {
timer = nil
}
public static func startPlayback(session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) {
public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) {
guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return }
if player != nil {
player?.destroy()
player = nil
@@ -76,8 +77,7 @@ class PlayerHandler {
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
self.session = session
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
startTickTimer()
}
@@ -108,7 +108,9 @@ class PlayerHandler {
}
public static func getPlaybackSession() -> PlaybackSession? {
self.player?.getPlaybackSession()
guard let player = player else { return nil }
guard let session = Database.shared.getPlaybackSession(id: player.getPlaybackSessionId()) else { return nil }
return session
}
public static func seekForward(amount: Double) {
@@ -164,8 +166,8 @@ class PlayerHandler {
}
public static func syncProgress() {
if session == nil { return }
guard let player = player else { return }
guard let session = getPlaybackSession() else { return }
// Prevent a sync at the current time
let playerCurrentTime = player.getCurrentTime()
@@ -185,15 +187,17 @@ class PlayerHandler {
lastSyncTime = Date().timeIntervalSince1970 // seconds
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
session!.currentTime = playerCurrentTime
session.update {
session.currentTime = playerCurrentTime
}
listeningTimePassedSinceLastSync = 0
lastSyncReport = report
let sessionIsLocal = session!.isLocal
let sessionIsLocal = session.isLocal
if !sessionIsLocal {
if Connectivity.isConnectedToInternet {
NSLog("sending playback report")
ApiClient.reportPlaybackProgress(report: report, sessionId: session!.id)
ApiClient.reportPlaybackProgress(report: report, sessionId: session.id)
}
} else {
if let localMediaProgress = syncLocalProgress() {
@@ -207,10 +211,10 @@ class PlayerHandler {
}
private static func syncLocalProgress() -> LocalMediaProgress? {
guard let session = session else { return nil }
guard let session = getPlaybackSession() else { return nil }
let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId)
guard var localMediaProgress = localMediaProgress else {
guard let localMediaProgress = localMediaProgress else {
// Local media progress should have been created
// If we're here, it means a library id is invalid
return nil