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.
let configuration = Realm.Configuration(
schemaVersion: 2,
schemaVersion: 3,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
NSLog("Realm schema version was \(oldSchemaVersion)")

View file

@ -8,7 +8,7 @@
import Foundation
import RealmSwift
class PlaybackSession: Object, Codable {
class PlaybackSession: Object, Codable, Deletable {
@Persisted(primaryKey: true) var id: String = ""
var userId: String?
@Persisted var libraryItemId: String?
@ -20,7 +20,7 @@ class PlaybackSession: Object, Codable {
@Persisted var displayAuthor: String?
@Persisted var coverPath: String?
@Persisted var duration: Double = 0
@Persisted var playMethod: Int = 1
@Persisted var playMethod: Int = PlayMethod.directplay.rawValue
@Persisted var startedAt: Double?
@Persisted var updatedAt: Double?
@Persisted var timeListening: Double = 0
@ -30,8 +30,24 @@ class PlaybackSession: Object, Codable {
@Persisted var localLibraryItem: LocalLibraryItem?
@Persisted var serverConnectionConfigId: String?
@Persisted var serverAddress: String?
@Persisted var isActiveSession = true
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? {
if let localLibraryItem = localLibraryItem, let episodeId = episodeId {
@ -72,10 +88,11 @@ class PlaybackSession: Object, Codable {
self.localLibraryItem = localLibraryItem
self.serverConnectionConfigId = serverConnectionConfigId
self.serverAddress = serverAddress
self.isActiveSession = true
}
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() {}
@ -107,6 +124,7 @@ class PlaybackSession: Object, Codable {
localLibraryItem = try values.decodeIfPresent(LocalLibraryItem.self, forKey: .localLibraryItem)
serverConnectionConfigId = try values.decodeIfPresent(String.self, forKey: .serverConnectionConfigId)
serverAddress = try values.decodeIfPresent(String.self, forKey: .serverAddress)
isActiveSession = true
}
func encode(to encoder: Encoder) throws {
@ -122,6 +140,8 @@ class PlaybackSession: Object, Codable {
try container.encode(coverPath, forKey: .coverPath)
try container.encode(duration, forKey: .duration)
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(updatedAt, forKey: .updatedAt)
try container.encode(timeListening, forKey: .timeListening)

View file

@ -34,7 +34,6 @@ class PlayerHandler {
}
}
private static var listeningTimePassedSinceLastSync: Double = 0.0
private static var lastSyncReport: PlaybackReport?
public static var paused: Bool {
get {
@ -69,15 +68,35 @@ class PlayerHandler {
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) {
guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return }
// Clean up the existing player
if player != nil {
player?.destroy()
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))
// Create the audio player
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
startTickTimer()
@ -93,6 +112,8 @@ class PlayerHandler {
player?.destroy()
player = nil
cleanupOldSessions(currentSessionId: nil)
NowPlayingInfo.shared.reset()
}
@ -180,10 +201,8 @@ class PlayerHandler {
guard let player = player else { return }
guard let session = getPlaybackSession() else { return }
// Prevent a sync at the current time
// Get current time
let playerCurrentTime = player.getCurrentTime()
let hasSyncAtCurrentTime = lastSyncReport?.currentTime.isEqual(to: playerCurrentTime) ?? false
if hasSyncAtCurrentTime { return }
// Prevent multiple sync requests
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
@ -196,29 +215,18 @@ class PlayerHandler {
guard !playerCurrentTime.isNaN else { return }
lastSyncTime = Date().timeIntervalSince1970 // seconds
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
session.update {
session.currentTime = playerCurrentTime
session.timeListening += listeningTimePassedSinceLastSync
}
listeningTimePassedSinceLastSync = 0
lastSyncReport = report
let sessionIsLocal = session.isLocal
if !sessionIsLocal {
if Connectivity.isConnectedToInternet {
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)")
}
}
}
// Persist items in the database and sync to the server
if session.isLocal {
_ = syncLocalProgress()
}
syncPlaybackSessionsToServer()
}
private static func syncLocalProgress() -> LocalMediaProgress? {
@ -241,4 +249,23 @@ class PlayerHandler {
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)
}
public static func reportLocalMediaProgress(_ localMediaProgress: LocalMediaProgress, callback: @escaping (_ success: Bool) -> Void) {
let progress = localMediaProgress.freeze()
public static func reportLocalPlaybackProgress(_ session: PlaybackSession, callback: @escaping (_ success: Bool) -> Void) {
let progress = session.freeze()
postResource(endpoint: "api/session/local", parameters: progress, callback: callback)
}