From 9477860bcadf86b3a5af645fd2521633067909e6 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 11 Aug 2022 18:29:55 -0400 Subject: [PATCH] Working local file playback --- ios/App/App/plugins/AbsAudioPlayer.swift | 16 ++++++--- ios/App/Shared/models/DataClasses.swift | 7 ++++ .../models/LocalLibraryExtensions.swift | 26 +++++++++------ ios/App/Shared/player/AudioPlayer.swift | 11 +++++++ ios/App/Shared/util/Database.swift | 5 +++ ios/App/Shared/util/NowPlayingInfo.swift | 33 +++++++++++++------ 6 files changed, 74 insertions(+), 24 deletions(-) diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index d4e74869..38ec9f76 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -45,11 +45,19 @@ public class AbsAudioPlayer: CAPPlugin { let isLocalItem = libraryItemId?.starts(with: "local_") ?? false if (isLocalItem) { let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!) - // TODO: Logic required for podcasts here - let playbackSession = item?.getPlaybackSession(episode: nil) + let episode = item?.getPodcastEpisode(episodeId: episodeId) + let playbackSession = item?.getPlaybackSession(episode: episode) + sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady) + do { + self.sendPlaybackSession(session: try playbackSession.asDictionary()) + call.resolve(try playbackSession.asDictionary()) + } catch(let exception) { + NSLog("failed to convert session to json") + debugPrint(exception) + call.resolve([:]) + } PlayerHandler.startPlayback(session: playbackSession!, playWhenReady: playWhenReady, playbackRate: playbackRate) self.sendMetadata() - call.resolve() } else { // Playing from the server sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady) ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in @@ -62,12 +70,12 @@ public class AbsAudioPlayer: CAPPlugin { call.resolve([:]) } - PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady, playbackRate: playbackRate) self.sendMetadata() } } } + @objc func closePlayback(_ call: CAPPluginCall) { NSLog("Close playback") diff --git a/ios/App/Shared/models/DataClasses.swift b/ios/App/Shared/models/DataClasses.swift index 578ee637..069b7818 100644 --- a/ios/App/Shared/models/DataClasses.swift +++ b/ios/App/Shared/models/DataClasses.swift @@ -90,6 +90,8 @@ struct Metadata: Realmable, Codable { var seriesName: String? var feedUrl: String? + var authorDisplayName: String { self.authorName ?? "Unknown" } + init() { title = "Unknown" genres = [] @@ -251,6 +253,11 @@ struct AudioTrack: Realmable, Codable { } return false } + + func getLocalFile() -> LocalFile? { + guard let localFileId = self.localFileId else { return nil } + return Database.shared.getLocalFile(localFileId: localFileId) + } } struct FileMetadata: Realmable, Codable { diff --git a/ios/App/Shared/models/LocalLibraryExtensions.swift b/ios/App/Shared/models/LocalLibraryExtensions.swift index 7dbc256a..501cc414 100644 --- a/ios/App/Shared/models/LocalLibraryExtensions.swift +++ b/ios/App/Shared/models/LocalLibraryExtensions.swift @@ -59,6 +59,12 @@ extension LocalLibraryItem { return total } + func getPodcastEpisode(episodeId: String?) -> PodcastEpisode? { + guard self.isPodcast else { return nil } + guard let episodes = self.media?.episodes else { return nil } + return episodes.first(where: { $0.id == episodeId }) + } + func getPlaybackSession(episode: PodcastEpisode?) -> PlaybackSession { let localEpisodeId = episode?.id let sessionId = "play_local_\(UUID().uuidString)" @@ -67,13 +73,13 @@ extension LocalLibraryItem { let mediaProgressId = (localEpisodeId != nil) ? "\(self.id)-\(localEpisodeId!)" : self.id let mediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: mediaProgressId) - // TODO: Clean up add mediaType methods for displayTitle and displayAuthor let mediaMetadata = self.media?.metadata - let audioTracks = self.media?.tracks - let authorName = mediaMetadata?.authorName + let chapters = self.media?.chapters + var audioTracks = self.media?.tracks + let authorName = mediaMetadata?.authorDisplayName - if let episode = episode { - // TODO: Implement podcast + if let episode = episode, let track = episode.audioTrack { + audioTracks = [track] } let dateNow = Date().timeIntervalSince1970 @@ -81,18 +87,18 @@ extension LocalLibraryItem { id: sessionId, userId: self.serverUserId, libraryItemId: self.libraryItemId, - episodeId: episode?.id, + episodeId: episode?.serverEpisodeId, mediaType: self.mediaType, - chapters: [], + chapters: chapters ?? [], displayTitle: mediaMetadata?.title, displayAuthor: authorName, - coverPath: nil, + coverPath: self.coverContentUrl, duration: self.getDuration(), - playMethod: 3, + playMethod: PlayMethod.local.rawValue, startedAt: dateNow, updatedAt: 0, timeListening: 0.0, - audioTracks: [], + audioTracks: audioTracks ?? [], currentTime: mediaProgress?.currentTime ?? 0.0, libraryItem: nil, serverConnectionConfigId: self.serverConnectionConfigId, diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index b6f8a7da..9f1aeb21 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -286,6 +286,17 @@ class AudioPlayer: NSObject { let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)" let url = URL(string: urlstr)! return AVURLAsset(url: url) + } else if (playbackSession.playMethod == PlayMethod.local.rawValue) { + guard let localFile = track.getLocalFile() else { + // Worst case we can stream the file + NSLog("Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")") + let filename = track.metadata?.filename ?? "" + let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) + let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)" + let url = URL(string: urlstr)! + return AVURLAsset(url: url) + } + return AVURLAsset(url: localFile.contentPath) } else { // HLS Transcode let headers: [String: String] = [ "Authorization": "Bearer \(Store.serverConfig!.token)" diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index d8a30851..f29c0742 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -131,6 +131,11 @@ class Database { } } + public func getLocalFile(localFileId: String) -> LocalFile? { + let realm = try! Realm() + return realm.object(ofType: LocalFile.self, forPrimaryKey: localFileId) + } + public func getDownloadItem(downloadItemId: String) -> DownloadItem? { let realm = try! Realm() return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId) diff --git a/ios/App/Shared/util/NowPlayingInfo.swift b/ios/App/Shared/util/NowPlayingInfo.swift index 71e0528a..fbb4181e 100644 --- a/ios/App/Shared/util/NowPlayingInfo.swift +++ b/ios/App/Shared/util/NowPlayingInfo.swift @@ -15,6 +15,10 @@ struct NowPlayingMetadata { var title: String var author: String? var series: String? + var coverUrl: URL? { + guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(itemId)/cover?token=\(Store.serverConfig!.token)") else { return nil } + return url + } } class NowPlayingInfo { @@ -30,18 +34,27 @@ class NowPlayingInfo { public func setSessionMetadata(metadata: NowPlayingMetadata) { setMetadata(artwork: nil, metadata: metadata) - guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else { - return - } - ApiClient.getData(from: url) { [self] image in - guard let downloadedImage = image else { - return + let isLocalItem = metadata.itemId.starts(with: "local_") + if isLocalItem { + guard let artworkUrl = metadata.artworkUrl else { return } + let coverImage = UIImage(contentsOfFile: artworkUrl) + guard let coverImage = coverImage else { return } + let artwork = MPMediaItemArtwork(boundsSize: coverImage.size) { _ -> UIImage in + return coverImage } - let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in - return downloadedImage - }) - self.setMetadata(artwork: artwork, metadata: metadata) + } else { + guard let url = metadata.coverUrl else { return } + ApiClient.getData(from: url) { [self] image in + guard let downloadedImage = image else { + return + } + let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in + return downloadedImage + }) + + self.setMetadata(artwork: artwork, metadata: metadata) + } } } public func update(duration: Double, currentTime: Double, rate: Float) {