Sync offline playback progress

This commit is contained in:
ronaldheft 2022-08-16 20:45:29 -04:00
parent 7c5ee940d3
commit 6aa0f2253b
4 changed files with 73 additions and 26 deletions

View file

@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Override point for customization after application launch. // Override point for customization after application launch.
let configuration = Realm.Configuration( let configuration = Realm.Configuration(
schemaVersion: 2, schemaVersion: 3,
migrationBlock: { migration, oldSchemaVersion in migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) { if (oldSchemaVersion < 1) {
NSLog("Realm schema version was \(oldSchemaVersion)") NSLog("Realm schema version was \(oldSchemaVersion)")

View file

@ -8,7 +8,7 @@
import Foundation import Foundation
import RealmSwift import RealmSwift
class PlaybackSession: Object, Codable { class PlaybackSession: Object, Codable, Deletable {
@Persisted(primaryKey: true) var id: String = "" @Persisted(primaryKey: true) var id: String = ""
var userId: String? var userId: String?
@Persisted var libraryItemId: String? @Persisted var libraryItemId: String?
@ -20,7 +20,7 @@ class PlaybackSession: Object, Codable {
@Persisted var displayAuthor: String? @Persisted var displayAuthor: String?
@Persisted var coverPath: String? @Persisted var coverPath: String?
@Persisted var duration: Double = 0 @Persisted var duration: Double = 0
@Persisted var playMethod: Int = 1 @Persisted var playMethod: Int = PlayMethod.directplay.rawValue
@Persisted var startedAt: Double? @Persisted var startedAt: Double?
@Persisted var updatedAt: Double? @Persisted var updatedAt: Double?
@Persisted var timeListening: Double = 0 @Persisted var timeListening: Double = 0
@ -30,8 +30,24 @@ class PlaybackSession: Object, Codable {
@Persisted var localLibraryItem: LocalLibraryItem? @Persisted var localLibraryItem: LocalLibraryItem?
@Persisted var serverConnectionConfigId: String? @Persisted var serverConnectionConfigId: String?
@Persisted var serverAddress: String? @Persisted var serverAddress: String?
@Persisted var isActiveSession = true
var isLocal: Bool { self.localLibraryItem != nil } var isLocal: Bool { self.localLibraryItem != nil }
var mediaPlayer: String { "AVPlayer" }
var deviceInfo: [String: String?]? {
var systemInfo = utsname()
uname(&systemInfo)
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
ptr in String.init(validatingUTF8: ptr)
}
}
return [
"manufacturer": "Apple",
"model": modelCode,
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
]
}
var localMediaProgressId: String? { var localMediaProgressId: String? {
if let localLibraryItem = localLibraryItem, let episodeId = episodeId { if let localLibraryItem = localLibraryItem, let episodeId = episodeId {
@ -72,10 +88,11 @@ class PlaybackSession: Object, Codable {
self.localLibraryItem = localLibraryItem self.localLibraryItem = localLibraryItem
self.serverConnectionConfigId = serverConnectionConfigId self.serverConnectionConfigId = serverConnectionConfigId
self.serverAddress = serverAddress self.serverAddress = serverAddress
self.isActiveSession = true
} }
private enum CodingKeys : String, CodingKey { private enum CodingKeys : String, CodingKey {
case id, userId, libraryItemId, episodeId, mediaType, chapters, displayTitle, displayAuthor, coverPath, duration, playMethod, startedAt, updatedAt, timeListening, audioTracks, currentTime, libraryItem, localLibraryItem, serverConnectionConfigId, serverAddress, isLocal, localMediaProgressId case id, userId, libraryItemId, episodeId, mediaType, chapters, displayTitle, displayAuthor, coverPath, duration, playMethod, mediaPlayer, deviceInfo, startedAt, updatedAt, timeListening, audioTracks, currentTime, libraryItem, localLibraryItem, serverConnectionConfigId, serverAddress, isLocal, localMediaProgressId
} }
override init() {} override init() {}
@ -107,6 +124,7 @@ class PlaybackSession: Object, Codable {
localLibraryItem = try values.decodeIfPresent(LocalLibraryItem.self, forKey: .localLibraryItem) localLibraryItem = try values.decodeIfPresent(LocalLibraryItem.self, forKey: .localLibraryItem)
serverConnectionConfigId = try values.decodeIfPresent(String.self, forKey: .serverConnectionConfigId) serverConnectionConfigId = try values.decodeIfPresent(String.self, forKey: .serverConnectionConfigId)
serverAddress = try values.decodeIfPresent(String.self, forKey: .serverAddress) serverAddress = try values.decodeIfPresent(String.self, forKey: .serverAddress)
isActiveSession = true
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
@ -122,6 +140,8 @@ class PlaybackSession: Object, Codable {
try container.encode(coverPath, forKey: .coverPath) try container.encode(coverPath, forKey: .coverPath)
try container.encode(duration, forKey: .duration) try container.encode(duration, forKey: .duration)
try container.encode(playMethod, forKey: .playMethod) try container.encode(playMethod, forKey: .playMethod)
try container.encode(mediaPlayer, forKey: .mediaPlayer)
try container.encode(deviceInfo, forKey: .deviceInfo)
try container.encode(startedAt, forKey: .startedAt) try container.encode(startedAt, forKey: .startedAt)
try container.encode(updatedAt, forKey: .updatedAt) try container.encode(updatedAt, forKey: .updatedAt)
try container.encode(timeListening, forKey: .timeListening) try container.encode(timeListening, forKey: .timeListening)

View file

@ -34,7 +34,6 @@ class PlayerHandler {
} }
} }
private static var listeningTimePassedSinceLastSync: Double = 0.0 private static var listeningTimePassedSinceLastSync: Double = 0.0
private static var lastSyncReport: PlaybackReport?
public static var paused: Bool { public static var paused: Bool {
get { get {
@ -69,15 +68,35 @@ class PlayerHandler {
timer = nil timer = nil
} }
private static func cleanupOldSessions(currentSessionId: String?) {
if let currentSessionId = currentSessionId {
let realm = try! Realm()
let oldSessions = realm.objects(PlaybackSession.self)
.where({ $0.isActiveSession == true && $0.id != currentSessionId })
try! realm.write {
for s in oldSessions {
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 }
// Clean up the existing player
if player != nil { if player != nil {
player?.destroy() player?.destroy()
player = nil player = nil
} }
// Cleanup old sessions
cleanupOldSessions(currentSessionId: sessionId)
// 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
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate) player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
startTickTimer() startTickTimer()
@ -93,6 +112,8 @@ class PlayerHandler {
player?.destroy() player?.destroy()
player = nil player = nil
cleanupOldSessions(currentSessionId: nil)
NowPlayingInfo.shared.reset() NowPlayingInfo.shared.reset()
} }
@ -180,10 +201,8 @@ class PlayerHandler {
guard let player = player else { return } guard let player = player else { return }
guard let session = getPlaybackSession() else { return } guard let session = getPlaybackSession() else { return }
// Prevent a sync at the current time // Get current time
let playerCurrentTime = player.getCurrentTime() let playerCurrentTime = player.getCurrentTime()
let hasSyncAtCurrentTime = lastSyncReport?.currentTime.isEqual(to: playerCurrentTime) ?? false
if hasSyncAtCurrentTime { return }
// Prevent multiple sync requests // Prevent multiple sync requests
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
@ -196,29 +215,18 @@ class PlayerHandler {
guard !playerCurrentTime.isNaN else { return } guard !playerCurrentTime.isNaN else { return }
lastSyncTime = Date().timeIntervalSince1970 // seconds lastSyncTime = Date().timeIntervalSince1970 // seconds
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
session.update { session.update {
session.currentTime = playerCurrentTime session.currentTime = playerCurrentTime
session.timeListening += listeningTimePassedSinceLastSync
} }
listeningTimePassedSinceLastSync = 0 listeningTimePassedSinceLastSync = 0
lastSyncReport = report
let sessionIsLocal = session.isLocal // Persist items in the database and sync to the server
if !sessionIsLocal { if session.isLocal {
if Connectivity.isConnectedToInternet { _ = syncLocalProgress()
NSLog("sending playback report")
ApiClient.reportPlaybackProgress(report: report, sessionId: session.id)
}
} else {
if let localMediaProgress = syncLocalProgress() {
if Connectivity.isConnectedToInternet {
ApiClient.reportLocalMediaProgress(localMediaProgress) { success in
NSLog("Synced local media progress: \(success)")
}
}
}
} }
syncPlaybackSessionsToServer()
} }
private static func syncLocalProgress() -> LocalMediaProgress? { private static func syncLocalProgress() -> LocalMediaProgress? {
@ -241,4 +249,23 @@ class PlayerHandler {
return localMediaProgress return localMediaProgress
} }
private static func syncPlaybackSessionsToServer() {
guard Connectivity.isConnectedToInternet else { return }
DispatchQueue.global(qos: .utility).async {
let realm = try! Realm()
for session in realm.objects(PlaybackSession.self) {
NSLog("Sending sessionId(\(session.id)) to server")
let sessionRef = ThreadSafeReference(to: session)
ApiClient.reportLocalPlaybackProgress(session.freeze()) { success in
// Remove old sessions after they synced with the server
let session = try! Realm().resolve(sessionRef)
if success && !(session?.isActiveSession ?? false) {
NSLog("Deleting sessionId(\(session!.id)) as is no longer active")
session?.delete()
}
}
}
}
}
} }

View file

@ -166,8 +166,8 @@ class ApiClient {
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil) try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
} }
public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) { public static func reportLocalPlaybackProgress(_ session: PlaybackSession, callback: @escaping (_ success: Bool) -> Void) {
let progress = localMediaProgress.freeze() let progress = session.freeze()
postResource(endpoint: "api/session/local", parameters: progress, callback: callback) postResource(endpoint: "api/session/local", parameters: progress, callback: callback)
} }