From 239938e0695e76e41daaf33ebf1a1f5570e29840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20Kr=C3=A4mer?= Date: Tue, 12 Apr 2022 14:28:47 +0200 Subject: [PATCH] Added PlaybackSession --- .../app/plugins/AbsAudioPlayer.kt | 2 +- ios/App/App.xcodeproj/project.pbxproj | 8 +-- ios/App/App/plugins/AbsDatabase.swift | 5 +- ios/App/Shared/models/Audiobook.swift | 24 -------- ios/App/Shared/models/PlaybackSession.swift | 57 +++++++++++++++++++ ios/App/Shared/player/AudioPlayer.swift | 23 +++++--- ios/App/Shared/player/PlayerHandler.swift | 9 ++- ios/App/Shared/util/NowPlayingInfo.swift | 40 +++++++------ ios/App/Shared/util/Store.swift | 13 ++++- 9 files changed, 121 insertions(+), 60 deletions(-) delete mode 100644 ios/App/Shared/models/Audiobook.swift create mode 100644 ios/App/Shared/models/PlaybackSession.swift diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index e27e3914..5094f7b6 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -24,8 +24,8 @@ class AbsAudioPlayer : Plugin() { lateinit var mainActivity: MainActivity lateinit var apiHandler:ApiHandler - lateinit var playerNotificationService: PlayerNotificationService + lateinit var playerNotificationService: PlayerNotificationService override fun load() { mainActivity = (activity as MainActivity) apiHandler = ApiHandler(mainActivity) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 831c1067..9f32c244 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -9,13 +9,13 @@ /* Begin PBXBuildFile section */ 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; 3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */; }; + 3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */; }; 3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF618E2804325C0070250E /* PlayerHandler.swift */; }; 3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */; }; 3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE628043E72006DB301 /* AbsDatabase.m */; }; 3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */; }; 3AD4FCEB280443DD006DB301 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEA280443DD006DB301 /* Database.swift */; }; 3AD4FCED28044E6C006DB301 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEC28044E6C006DB301 /* Store.swift */; }; - 3AD4FCEF28045D7C006DB301 /* Audiobook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEE28045D7C006DB301 /* Audiobook.swift */; }; 3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; @@ -29,13 +29,13 @@ /* Begin PBXFileReference section */ 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; + 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSession.swift; sourceTree = ""; }; 3ABF618E2804325C0070250E /* PlayerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerHandler.swift; sourceTree = ""; }; 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDatabase.swift; sourceTree = ""; }; 3AD4FCE628043E72006DB301 /* AbsDatabase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDatabase.m; sourceTree = ""; }; 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnectionConfig.swift; sourceTree = ""; }; 3AD4FCEA280443DD006DB301 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; 3AD4FCEC28044E6C006DB301 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; - 3AD4FCEE28045D7C006DB301 /* Audiobook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Audiobook.swift; sourceTree = ""; }; 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = ""; }; 4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; @@ -102,8 +102,8 @@ 3AFCB5E527EA232A00ECCC05 /* models */ = { isa = PBXGroup; children = ( - 3AD4FCEE28045D7C006DB301 /* Audiobook.swift */, 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */, + 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */, ); path = models; sourceTree = ""; @@ -278,10 +278,10 @@ files = ( 3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */, 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + 3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */, 3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */, 3AD4FCED28044E6C006DB301 /* Store.swift in Sources */, 3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */, - 3AD4FCEF28045D7C006DB301 /* Audiobook.swift in Sources */, 3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */, 3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */, 3AD4FCEB280443DD006DB301 /* Database.swift in Sources */, diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 6e9f643e..bfc49800 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -62,7 +62,10 @@ public class AbsDatabase: CAPPlugin { "serverConnectionConfigs": configs.map { config in return convertServerConnectionConfigToJSON(config: config) }, - "lastServerConnectionConfigId": index < 0 ? -1 : configs[index].id, + "lastServerConnectionConfigId": index < 0 ? -1 : configs.first(where: { + (config: ServerConnectionConfig) -> Bool in + return config.index == index + })!.id, "currentLocalPlaybackSession": nil, // Luckily this isn't implemented yet ]) } diff --git a/ios/App/Shared/models/Audiobook.swift b/ios/App/Shared/models/Audiobook.swift deleted file mode 100644 index e67465a0..00000000 --- a/ios/App/Shared/models/Audiobook.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// AudioBook.swift -// App -// -// Created by Rasmus Krämer on 07.03.22. -// - -import Foundation - -struct Audiobook { - var streamId: String - var audiobookId: String - var playlistUrl: String - - var startTime: Double = 0.0 - var duration: Double - - var title: String - var series: String? - var author: String? - var artworkUrl: String? - - var token: String -} diff --git a/ios/App/Shared/models/PlaybackSession.swift b/ios/App/Shared/models/PlaybackSession.swift new file mode 100644 index 00000000..769cea4b --- /dev/null +++ b/ios/App/Shared/models/PlaybackSession.swift @@ -0,0 +1,57 @@ +// +// PlaybackSession.swift +// App +// +// Created by Rasmus Krämer on 12.04.22. +// + +import Foundation + +struct PlaybackSession { + var id: String + var userId: String? + var libraryItemId: String? + var episodeId: String? + var mediaType: String + // var mediaMetadata: MediaTypeMetadata - It is not implemented in android? + var chapters: [Chapter] + var displayTitle: String? + var displayAuthor: String? + var coverPath: String? + var duration: Double + var playMethod: Int + var startedAt: Double + var updatedAt: Double + var timeListening: Double + var audioTracks: [AudioTrack] + var currentTime: Double + // var libraryItem: LibraryItem? + // var localLibraryItem: LocalLibraryItem? + var serverConnectionConfigId: String? + var serverAddress: String? +} +struct Chapter { + var id: Int + var start: Double + var end: Double + var title: String? +} +struct AudioTrack { + var index: Int + var startOffset: Double + var duration: Double + var title: String + var contentUrl: String + var mimeType: String + var metadata: FileMetadata? + // var isLocal: Bool + // var localFileId: String? + // var audioProbeResult: AudioProbeResult? Need for local playback + var serverIndex: Int? +} +struct FileMetadata { + var filename: String + var ext: String + var path: String + var relPath: String +} diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 49bc9bcc..246c3d7a 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -24,21 +24,30 @@ class AudioPlayer: NSObject { private var playWhenReady: Bool private var audioPlayer: AVPlayer - public var audiobook: Audiobook + private var playbackSession: PlaybackSession + private var activeAudioTrack: AudioTrack // MARK: - Constructor - init(audiobook: Audiobook, playWhenReady: Bool = false) { + init(playbackSession: PlaybackSession, playWhenReady: Bool = false) { self.playWhenReady = playWhenReady - self.audiobook = audiobook self.audioPlayer = AVPlayer() + self.playbackSession = playbackSession self.status = -1 self.rate = 0.0 + if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" { + NSLog("The player only support HLS streams right now") + self.activeAudioTrack = AudioTrack(index: 0, startOffset: -1, duration: -1, title: "", contentUrl: "", mimeType: "") + + super.init() + return + } + self.activeAudioTrack = playbackSession.audioTracks[0] + super.init() initAudioSession() setupRemoteTransportControls() - NowPlayingInfo.setAudiobook(audiobook: audiobook) // Listen to player events self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext) @@ -48,7 +57,7 @@ class AudioPlayer: NSObject { playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: &playerItemContext) self.audioPlayer.replaceCurrentItem(with: playerItem) - seek(self.audiobook.startTime) + seek(self.playbackSession.currentTime) NSLog("Audioplayer ready") } @@ -153,10 +162,10 @@ class AudioPlayer: NSObject { // MARK: - Private private func createAsset() -> AVAsset { let headers: [String: String] = [ - "Authorization": "Bearer \(audiobook.token)" + "Authorization": "Bearer \(Store.serverConfig.token)" ] - return AVURLAsset(url: URL(string: audiobook.playlistUrl)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) + return AVURLAsset(url: URL(string: activeAudioTrack.contentUrl)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) } private func initAudioSession() { do { diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index dee0097f..7e498bd1 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -9,9 +9,12 @@ import Foundation class PlayerHandler { private static var player: AudioPlayer? - // private static var item: any + private static var session: PlaybackSession? - public static func setItem() { - + public static func startPlayback(session: PlaybackSession) { + if player != nil { + player?.destroy() + player = nil + } } } diff --git a/ios/App/Shared/util/NowPlayingInfo.swift b/ios/App/Shared/util/NowPlayingInfo.swift index 3af49275..cd6f2e3b 100644 --- a/ios/App/Shared/util/NowPlayingInfo.swift +++ b/ios/App/Shared/util/NowPlayingInfo.swift @@ -16,19 +16,25 @@ func getData(from url: URL, completion: @escaping (UIImage?) -> Void) { }).resume() } +struct NowPlayingMetadata { + var id: String + var artworkUrl: String? + var title: String + var author: String? + var series: String? +} + class NowPlayingInfo { private static var nowPlayingInfo: [String: Any] = [:] - private static var audiobook: Audiobook? - public static func setAudiobook(audiobook: Audiobook) { - self.audiobook = audiobook - setMetadata(nil) + public static func setAudiobook(metadata: NowPlayingMetadata) { + setMetadata(artwork: nil, metadata: nil) - if !shouldFetchCover() || audiobook.artworkUrl == nil { + if !shouldFetchCover(id: metadata.id) || metadata.artworkUrl == nil { return } - guard let url = URL(string: audiobook.artworkUrl!) else { return } + guard let url = URL(string: metadata.artworkUrl!) else { return } getData(from: url) { [self] image in guard let downloadedImage = image else { return @@ -37,7 +43,7 @@ class NowPlayingInfo { return downloadedImage }) - self.setMetadata(artwork) + self.setMetadata(artwork: artwork, metadata: metadata) } } public static func update(duration: Double, currentTime: Double, rate: Float) { @@ -49,31 +55,29 @@ class NowPlayingInfo { MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } public static func reset() { - audiobook = nil MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } - private static func setMetadata(_ artwork: MPMediaItemArtwork?) { - if self.audiobook == nil { + private static func setMetadata(artwork: MPMediaItemArtwork?, metadata: NowPlayingMetadata?) { + if metadata == nil { return } if artwork != nil { nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork - } else if shouldFetchCover() { + } else if shouldFetchCover(id: metadata!.id) { nowPlayingInfo[MPMediaItemPropertyArtwork] = nil } - nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = audiobook!.streamId - nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = URL(string: audiobook!.playlistUrl) + nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = metadata!.id nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls" - nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook!.title - nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook!.author ?? "unknown" - nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = audiobook!.series + nowPlayingInfo[MPMediaItemPropertyTitle] = metadata!.title + nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown" + nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series } - private static func shouldFetchCover() -> Bool { - audiobook != nil && (nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != audiobook!.streamId || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil) + private static func shouldFetchCover(id: String) -> Bool { + nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil } } diff --git a/ios/App/Shared/util/Store.swift b/ios/App/Shared/util/Store.swift index bdedadce..f7ab787c 100644 --- a/ios/App/Shared/util/Store.swift +++ b/ios/App/Shared/util/Store.swift @@ -9,14 +9,23 @@ import Foundation import RealmSwift class Store { + private static var _serverConfig: ServerConnectionConfig? // ONLY USE REALM IN Database.realmQueue OR ELSE THE APP WILL CRASH public static var serverConfig: ServerConnectionConfig { get { - // TODO: change this when multiple configs are possible - Database.getServerConnectionConfigs()[Database.getActiveServerConfigIndex()] + if _serverConfig == nil { + let index = Database.getActiveServerConfigIndex() + // TODO: change this when multiple configs are possible + _serverConfig = Database.getServerConnectionConfigs().first { config in + return config.index == index + } + } + + return _serverConfig ?? ServerConnectionConfig() } set(updated) { Database.setServerConnectionConfig(config: updated) + _serverConfig = updated } } }