diff --git a/Extensions/CMTime+DefaultTimescale.swift b/Extensions/CMTime+DefaultTimescale.swift new file mode 100644 index 00000000..0656dd6b --- /dev/null +++ b/Extensions/CMTime+DefaultTimescale.swift @@ -0,0 +1,10 @@ +import CoreMedia +import Foundation + +extension CMTime { + static let defaultTimescale: CMTimeScale = 1_000_000 + + static func secondsInDefaultTimescale(_ seconds: TimeInterval) -> CMTime { + CMTime(seconds: seconds, preferredTimescale: CMTime.defaultTimescale) + } +} diff --git a/Model/Applications/VideosAPI.swift b/Model/Applications/VideosAPI.swift index c23017f9..ee0d08f6 100644 --- a/Model/Applications/VideosAPI.swift +++ b/Model/Applications/VideosAPI.swift @@ -23,4 +23,26 @@ protocol VideosAPI { func playlistVideos(_ id: String) -> Resource? func channelPlaylist(_ id: String) -> Resource? + + func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void) +} + +extension VideosAPI { + func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void = { _ in }) { + guard (item.video?.streams ?? []).isEmpty else { + completionHandler(item) + return + } + + video(item.videoID).load().onSuccess { response in + guard let video: Video = response.typedContent() else { + return + } + + var newItem = item + newItem.video = video + + completionHandler(newItem) + } + } } diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index bc9b8760..8c19ae3f 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -26,10 +26,10 @@ final class PlayerModel: ObservableObject { @Published var availableStreams = [Stream]() { didSet { rebuildTVMenu() } } @Published var streamSelection: Stream? { didSet { rebuildTVMenu() } } - @Published var queue = [PlayerQueueItem]() - @Published var currentItem: PlayerQueueItem! + @Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } } + @Published var currentItem: PlayerQueueItem! { didSet { Defaults[.lastPlayed] = currentItem } } + @Published var history = [PlayerQueueItem]() { didSet { Defaults[.history] = history } } - @Published var history = [PlayerQueueItem]() @Published var savedTime: CMTime? @Published var playerNavigationLinkActive = false @@ -43,23 +43,54 @@ final class PlayerModel: ObservableObject { var instances: InstancesModel var composition = AVMutableComposition() - var timeObserver: Any? - private var shouldResumePlaying = true + + private var frequentTimeObserver: Any? + private var infrequentTimeObserver: Any? + private var playerTimeControlStatusObserver: Any? + private var statusObservation: NSKeyValueObservation? - #if os(macOS) - var playerTimeControlStatusObserver: Any? - #endif + var autoPlayItems = false init(accounts: AccountsModel? = nil, instances: InstancesModel? = nil) { self.accounts = accounts ?? AccountsModel() self.instances = instances ?? InstancesModel() addItemDidPlayToEndTimeObserver() - addTimeObserver() + addFrequentTimeObserver() + addInfrequentTimeObserver() + addPlayerTimeControlStatusObserver() + } - #if os(macOS) - addPlayerTimeControlStatusObserver() - #endif + func loadHistoryDetails() { + guard !accounts.current.isNil else { + return + } + + queue = Defaults[.queue] + queue.forEach { item in + accounts.api.loadDetails(item) { newItem in + if let index = self.queue.firstIndex(where: { $0.id == item.id }) { + self.queue[index] = newItem + } + } + } + + history = Defaults[.history] + history.forEach { item in + accounts.api.loadDetails(item) { newItem in + if let index = self.history.firstIndex(where: { $0.id == item.id }) { + self.history[index] = newItem + } + } + } + + if let item = Defaults[.lastPlayed] { + accounts.api.loadDetails(item) { [weak self] newItem in + self?.playNow(newItem.video, at: newItem.playbackTime?.seconds) + } + } else { + autoPlayItems = true + } } func presentPlayer() { @@ -75,7 +106,7 @@ final class PlayerModel: ObservableObject { } var live: Bool { - currentItem?.video.live ?? false + currentItem?.video?.live ?? false } var playerItemDuration: CMTime? { @@ -91,7 +122,7 @@ final class PlayerModel: ObservableObject { } func play() { - guard !isPlaying else { + guard player.timeControlStatus != .playing else { return } @@ -99,34 +130,20 @@ final class PlayerModel: ObservableObject { } func pause() { - guard isPlaying else { + guard player.timeControlStatus != .paused else { return } player.pause() } - func playVideo(_ video: Video, time: CMTime? = nil) { - savedTime = time - shouldResumePlaying = true - - loadAvailableStreams(video) { streams in - guard let stream = streams.first else { - return - } - - self.streamSelection = stream - self.playStream(stream, of: video, preservingTime: !time.isNil) - } - } - func upgradeToStream(_ stream: Stream) { if !self.stream.isNil, self.stream != stream { playStream(stream, of: currentVideo!, preservingTime: true) } } - private func playStream( + func playStream( _ stream: Stream, of video: Video, preservingTime: Bool = false @@ -164,11 +181,33 @@ final class PlayerModel: ObservableObject { attachMetadata(to: playerItem!, video: video, for: stream) - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } + self.stream = stream self.composition = AVMutableComposition() } + let startPlaying = { + #if !os(macOS) + try? AVAudioSession.sharedInstance().setActive(true) + #endif + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + guard let self = self else { + return + } + + guard self.autoPlayItems else { + return + } + + self.play() + } + } + let replaceItemAndSeek = { self.player.replaceCurrentItem(with: playerItem) self.seekToSavedTime { finished in @@ -176,19 +215,8 @@ final class PlayerModel: ObservableObject { return } self.savedTime = nil - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - self.play() - } - } - } - let startPlaying = { - #if !os(macOS) - try? AVAudioSession.sharedInstance().setActive(true) - #endif - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - self.play() + startPlaying() } } @@ -224,7 +252,11 @@ final class PlayerModel: ObservableObject { insertPlayerItem(stream, for: video, preservingTime: preservingTime) } - private func loadCompositionAsset(_ asset: AVURLAsset, type: AVMediaType, of video: Video) async { + private func loadCompositionAsset( + _ asset: AVURLAsset, + type: AVMediaType, + of video: Video + ) async { async let assetTracks = asset.loadTracks(withMediaType: type) logger.info("loading \(type.rawValue) track") @@ -242,7 +274,7 @@ final class PlayerModel: ObservableObject { } try! compositionTrack.insertTimeRange( - CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1_000_000)), + CMTimeRange(start: .zero, duration: CMTime.secondsInDefaultTimescale(video.length)), of: assetTrack, at: .zero ) @@ -279,10 +311,14 @@ final class PlayerModel: ObservableObject { item.preferredForwardBufferDuration = 5 statusObservation?.invalidate() - statusObservation = item.observe(\.status, options: [.old, .new]) { playerItem, _ in + statusObservation = item.observe(\.status, options: [.old, .new]) { [weak self] playerItem, _ in + guard let self = self else { + return + } + switch playerItem.status { case .readyToPlay: - if self.isAutoplaying(playerItem), self.shouldResumePlaying { + if self.isAutoplaying(playerItem) { self.play() } case .failed: @@ -321,12 +357,14 @@ final class PlayerModel: ObservableObject { try? AVAudioSession.sharedInstance().setActive(false) #endif + currentItem.playbackTime = playerItemDuration + if queue.isEmpty { addCurrentItemToHistory() resetQueue() #if os(tvOS) - avPlayerViewController!.dismiss(animated: true) { - self.controller!.dismiss(animated: true) + avPlayerViewController!.dismiss(animated: true) { [weak self] in + self?.controller!.dismiss(animated: true) } #endif presentingPlayer = false @@ -342,8 +380,8 @@ final class PlayerModel: ObservableObject { return } - DispatchQueue.main.async { - self.savedTime = currentTime + DispatchQueue.main.async { [weak self] in + self?.savedTime = currentTime completionHandler() } } @@ -355,43 +393,74 @@ final class PlayerModel: ObservableObject { player.seek( to: time, - toleranceBefore: .init(seconds: 1, preferredTimescale: 1_000_000), + toleranceBefore: .secondsInDefaultTimescale(1), toleranceAfter: .zero, completionHandler: completionHandler ) } - private func addTimeObserver() { - let interval = CMTime(seconds: 0.5, preferredTimescale: 1_000_000) + private func addFrequentTimeObserver() { + let interval = CMTime.secondsInDefaultTimescale(0.5) + + frequentTimeObserver = player.addPeriodicTimeObserver( + forInterval: interval, + queue: .main + ) { [weak self] _ in + guard let self = self else { + return + } - timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in self.currentRate = self.player.rate guard !self.currentItem.isNil else { return } - let time = self.player.currentTime() - - self.currentItem!.playbackTime = time - self.currentItem!.videoDuration = self.player.currentItem?.asset.duration.seconds - - self.handleSegments(at: time) + self.handleSegments(at: self.player.currentTime()) } } - #if os(macOS) - private func addPlayerTimeControlStatusObserver() { - playerTimeControlStatusObserver = player.observe(\.timeControlStatus) { player, _ in - guard self.player == player else { - return - } + private func addInfrequentTimeObserver() { + let interval = CMTime.secondsInDefaultTimescale(5) + + infrequentTimeObserver = player.addPeriodicTimeObserver( + forInterval: interval, + queue: .main + ) { [weak self] _ in + guard let self = self else { + return + } + + guard !self.currentItem.isNil else { + return + } + + self.updateCurrentItemIntervals() + } + } + + private func addPlayerTimeControlStatusObserver() { + playerTimeControlStatusObserver = player.observe(\.timeControlStatus) { [weak self] player, _ in + guard let self = self, + self.player == player + else { + return + } + + #if os(macOS) if player.timeControlStatus == .playing { ScreenSaverManager.shared.disable(reason: "Yattee is playing video") } else { ScreenSaverManager.shared.enable() } - } + #endif + + self.updateCurrentItemIntervals() } - #endif + } + + private func updateCurrentItemIntervals() { + currentItem?.playbackTime = player.currentTime() + currentItem?.videoDuration = player.currentItem?.asset.duration.seconds + } } diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index a9da5f5f..ccee3424 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -39,7 +39,7 @@ extension PlayerModel { currentItem = item if !time.isNil { - currentItem.playbackTime = CMTime(seconds: time!, preferredTimescale: 1_000_000) + currentItem.playbackTime = .secondsInDefaultTimescale(time!) } else if currentItem.playbackTime.isNil { currentItem.playbackTime = .zero } @@ -48,7 +48,20 @@ extension PlayerModel { currentItem.video = video! } - playVideo(currentVideo!, time: currentItem.playbackTime) + savedTime = currentItem.playbackTime + + loadAvailableStreams(currentVideo!) { streams in + guard let stream = streams.first else { + return + } + + self.streamSelection = stream + self.playStream( + stream, + of: self.currentVideo!, + preservingTime: !self.currentItem.playbackTime.isNil + ) + } } func advanceToNextItem() { @@ -62,9 +75,10 @@ extension PlayerModel { func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) { addCurrentItemToHistory() - let item = remove(newItem)! - loadDetails(newItem.video) { video in - self.playItem(item, video: video, at: time) + remove(newItem) + + accounts.api.loadDetails(newItem) { newItem in + self.playItem(newItem, video: newItem.video, at: time) } } @@ -77,18 +91,30 @@ extension PlayerModel { } func resetQueue() { - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } + self.currentItem = nil self.stream = nil self.removeQueueItems() - self.timeObserver = nil } player.replaceCurrentItem(with: nil) } func isAutoplaying(_ item: AVPlayerItem) -> Bool { - player.currentItem == item + guard player.currentItem == item else { + return false + } + + if !autoPlayItems { + autoPlayItems = true + return false + } + + return true } @discardableResult func enqueueVideo( @@ -102,36 +128,24 @@ extension PlayerModel { queue.insert(item, at: prepending ? 0 : queue.endIndex) - loadDetails(video) { video in - videoDetailsLoadHandler(video, item) + accounts.api.loadDetails(item) { newItem in + videoDetailsLoadHandler(newItem.video, newItem) if play { - self.playItem(item, video: video) + self.playItem(newItem, video: video) } } return item } - private func loadDetails(_ video: Video?, onSuccess: @escaping (Video) -> Void) { - guard video != nil else { - return - } - - accounts.api.video(video!.videoID).load().onSuccess { response in - if let video: Video = response.typedContent() { - onSuccess(video) - } - } - } - func addCurrentItemToHistory() { if let item = currentItem { - if let index = history.firstIndex(where: { $0.video.videoID == item.video.videoID }) { + if let index = history.firstIndex(where: { $0.video.videoID == item.video?.videoID }) { history.remove(at: index) } - history.insert(item, at: 0) + history.insert(currentItem, at: 0) } } diff --git a/Model/Player/PlayerQueueItem.swift b/Model/Player/PlayerQueueItem.swift index 806ddd1d..400fc0bd 100644 --- a/Model/Player/PlayerQueueItem.swift +++ b/Model/Player/PlayerQueueItem.swift @@ -1,14 +1,19 @@ import AVFoundation +import Defaults import Foundation -struct PlayerQueueItem: Hashable, Identifiable { +struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable { + static let bridge = PlayerQueueItemBridge() + var id = UUID() - var video: Video + var video: Video! + var videoID: Video.ID var playbackTime: CMTime? var videoDuration: TimeInterval? - init(_ video: Video, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) { + init(_ video: Video? = nil, videoID: Video.ID? = nil, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) { self.video = video + self.videoID = videoID ?? video!.videoID self.playbackTime = playbackTime self.videoDuration = videoDuration } diff --git a/Model/Player/PlayerQueueItemBridge.swift b/Model/Player/PlayerQueueItemBridge.swift new file mode 100644 index 00000000..ab43a49a --- /dev/null +++ b/Model/Player/PlayerQueueItemBridge.swift @@ -0,0 +1,67 @@ +import CoreMedia +import Defaults +import Foundation + +struct PlayerQueueItemBridge: Defaults.Bridge { + typealias Value = PlayerQueueItem + typealias Serializable = [String: String] + + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + let videoID = value.videoID.isEmpty ? value.video!.videoID : value.videoID + + var playbackTime = "" + if let time = value.playbackTime { + if time.seconds.isFinite { + playbackTime = String(time.seconds) + } + } + + var videoDuration = "" + if let duration = value.videoDuration { + if duration.isFinite { + videoDuration = String(duration) + } + } + + return [ + "videoID": videoID, + "playbackTime": playbackTime, + "videoDuration": videoDuration + ] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let videoID = object["videoID"] + else { + return nil + } + + var playbackTime: CMTime? + var videoDuration: TimeInterval? + + if let time = object["playbackTime"], + !time.isEmpty, + let seconds = TimeInterval(time) + { + playbackTime = .secondsInDefaultTimescale(seconds) + } + + if let duration = object["videoDuration"], + !duration.isEmpty + { + videoDuration = TimeInterval(duration) + } + + return PlayerQueueItem( + videoID: videoID, + playbackTime: playbackTime, + videoDuration: videoDuration + ) + } +} diff --git a/Model/Player/PlayerSegments.swift b/Model/Player/PlayerSponsorBlock.swift similarity index 92% rename from Model/Player/PlayerSegments.swift rename to Model/Player/PlayerSponsorBlock.swift index 365731d4..aff4ad9f 100644 --- a/Model/Player/PlayerSegments.swift +++ b/Model/Player/PlayerSponsorBlock.swift @@ -5,7 +5,7 @@ import Foundation extension PlayerModel { func handleSegments(at time: CMTime) { if let segment = lastSkipped { - if time > CMTime(seconds: segment.end + 10, preferredTimescale: 1_000_000) { + if time > .secondsInDefaultTimescale(segment.end + 10) { resetLastSegment() } } @@ -18,7 +18,7 @@ extension PlayerModel { var nextSegments = [firstSegment] while let segment = sponsorBlock.segments.first(where: { - $0.timeInSegment(CMTime(seconds: nextSegments.last!.end + 2, preferredTimescale: 1_000_000)) + $0.timeInSegment(.secondsInDefaultTimescale(nextSegments.last!.end + 2)) }) { nextSegments.append(segment) } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 0d171da5..ec26b13b 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -129,9 +129,9 @@ 374C053B2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; }; 374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; }; 374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; }; - 374C053F272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; }; - 374C0540272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; }; - 374C0541272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; }; + 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */; }; + 374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */; }; + 374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */; }; 374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C0542272496E4009BDDBE /* AppDelegate.swift */; }; 375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; @@ -302,6 +302,12 @@ 37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; }; 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; }; 37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */; }; + 37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; }; + 37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; }; + 37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; }; + 37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; }; + 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; }; + 37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; }; 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; 37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; 37C3A241272359900087A57A /* Double+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C3A240272359900087A57A /* Double+Format.swift */; }; @@ -509,7 +515,7 @@ 37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = ""; }; 374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.swift; sourceTree = ""; }; 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTVMenu.swift; sourceTree = ""; }; - 374C053E272472C0009BDDBE /* PlayerSegments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSegments.swift; sourceTree = ""; }; + 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSponsorBlock.swift; sourceTree = ""; }; 374C0542272496E4009BDDBE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = macOS/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; 374C0544272496FD009BDDBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; @@ -577,6 +583,8 @@ 37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = ""; }; + 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItemBridge.swift; sourceTree = ""; }; + 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMTime+DefaultTimescale.swift"; sourceTree = ""; }; 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsModel.swift; sourceTree = ""; }; 37C3A240272359900087A57A /* Double+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Format.swift"; sourceTree = ""; }; 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylist.swift; sourceTree = ""; }; @@ -814,7 +822,8 @@ 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */, 37319F0427103F94004ECCD0 /* PlayerQueue.swift */, 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */, - 374C053E272472C0009BDDBE /* PlayerSegments.swift */, + 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */, + 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */, 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */, 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */, 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */, @@ -954,6 +963,7 @@ children = ( 379775922689365600DD52A8 /* Array+Next.swift */, 376578842685429C00D4EA09 /* CaseIterable+Next.swift */, + 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */, 37C3A240272359900087A57A /* Double+Format.swift */, 37BA794E26DC3E0E002A0235 /* Int+Format.swift */, 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */, @@ -1643,7 +1653,7 @@ 377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */, 376578912685490700D4EA09 /* PlaylistsView.swift in Sources */, 37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */, - 374C053F272472C0009BDDBE /* PlayerSegments.swift in Sources */, + 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */, 37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37484C2D26FC844700287258 /* AccountsSettings.swift in Sources */, @@ -1651,6 +1661,7 @@ 37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */, 37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */, 376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */, + 37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */, 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */, @@ -1688,6 +1699,7 @@ 37E70923271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 37484C1D26FC83A400287258 /* InstancesSettings.swift in Sources */, 37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, + 37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, 37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */, 37C3A241272359900087A57A /* Double+Format.swift in Sources */, 37FB285E272225E800A57617 /* ContentItemView.swift in Sources */, @@ -1706,7 +1718,7 @@ 37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */, 3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, - 374C0540272472C0009BDDBE /* PlayerSegments.swift in Sources */, + 374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */, 37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, 37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, @@ -1767,6 +1779,7 @@ 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */, 37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, + 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 3761AC1026F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */, 37732FF52703D32400F04329 /* Sidebar.swift in Sources */, @@ -1804,6 +1817,7 @@ 3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */, 373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */, + 37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, 37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, @@ -1861,6 +1875,7 @@ 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */, 37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */, + 37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, 37C3A243272359900087A57A /* Double+Format.swift in Sources */, 37AAF29226740715007FC770 /* Channel.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, @@ -1882,7 +1897,7 @@ 3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */, 37001565271B1F250049C794 /* AccountsModel.swift in Sources */, - 374C0541272472C0009BDDBE /* PlayerSegments.swift in Sources */, + 374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, 376A33E62720CB35000C1D6B /* Account.swift in Sources */, 37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */, @@ -1907,6 +1922,7 @@ 37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */, 37FB2860272225E800A57617 /* ContentItemView.swift in Sources */, 374C053727242D9F009BDDBE /* ServicesSettings.swift in Sources */, + 37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */, 3711404126B206A6005B3555 /* SearchModel.swift in Sources */, 37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */, diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 53d65f55..f3c32b13 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -29,6 +29,10 @@ extension Defaults.Keys { static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) + static let queue = Key<[PlayerQueueItem]>("queue", default: []) + static let history = Key<[PlayerQueueItem]>("history", default: []) + static let lastPlayed = Key("lastPlayed") + static let trendingCategory = Key("trendingCategory", default: .default) static let trendingCountry = Key("trendingCountry", default: .us) } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 0a656c22..847045ef 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -111,6 +111,10 @@ struct ContentView: View { playlists.accounts = accounts search.accounts = accounts subscriptions.accounts = accounts + + if !accounts.current.isNil { + player.loadHistoryDetails() + } } func openWelcomeScreenIfAccountEmpty() { @@ -135,6 +139,7 @@ struct ContentView: View { accounts.api.video(id).load().onSuccess { response in if let video: Video = response.typedContent() { + self.player.autoPlayItems = true self.player.playNow(video, at: parser.time) self.player.presentPlayer() } diff --git a/Shared/Player/PlaybackBar.swift b/Shared/Player/PlaybackBar.swift index 8928411b..6c3fa051 100644 --- a/Shared/Player/PlaybackBar.swift +++ b/Shared/Player/PlaybackBar.swift @@ -24,7 +24,7 @@ struct PlaybackBar: View { if !player.lastSkipped.isNil { restoreLastSkippedSegmentButton } - if player.currentVideo!.live { + if player.live { Image(systemName: "dot.radiowaves.left.and.right") } else if player.isLoadingAvailableStreams || player.isLoadingStream { Image(systemName: "bolt.horizontal.fill") @@ -78,7 +78,7 @@ struct PlaybackBar: View { return "LIVE" } - guard player.time != nil, player.time!.isValid else { + guard player.time != nil, player.time!.isValid, !player.currentVideo.isNil else { return "loading..." } diff --git a/Shared/Videos/VideoBanner.swift b/Shared/Videos/VideoBanner.swift index c2bb6c08..12725321 100644 --- a/Shared/Videos/VideoBanner.swift +++ b/Shared/Videos/VideoBanner.swift @@ -4,11 +4,11 @@ import SDWebImageSwiftUI import SwiftUI struct VideoBanner: View { - let video: Video + let video: Video? var playbackTime: CMTime? var videoDuration: TimeInterval? - init(video: Video, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) { + init(video: Video? = nil, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) { self.video = video self.playbackTime = playbackTime self.videoDuration = videoDuration @@ -24,14 +24,14 @@ struct VideoBanner: View { #endif } VStack(alignment: .leading, spacing: 4) { - Text(video.title) + Text(video?.title ?? "Unknown title") .truncationMode(.middle) .lineLimit(2) .font(.headline) .frame(alignment: .leading) HStack { - Text(video.author) + Text(video?.author ?? "Unknown author") .lineLimit(1) Spacer() @@ -40,7 +40,7 @@ struct VideoBanner: View { progressView #endif - if let time = (videoDuration ?? video.length).formattedAsPlaybackTime() { + if let time = (videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() { Text(time) .fontWeight(.light) } @@ -71,7 +71,7 @@ struct VideoBanner: View { } private var smallThumbnail: some View { - WebImage(url: video.thumbnailURL(quality: .medium)) + WebImage(url: video?.thumbnailURL(quality: .medium)) .resizable() .placeholder { ProgressView() @@ -109,7 +109,7 @@ struct VideoBanner: View { } private var progressViewTotal: Double { - videoDuration ?? video.length + videoDuration ?? video?.length ?? 1 } } diff --git a/Shared/Views/PlayerControlsView.swift b/Shared/Views/PlayerControlsView.swift index 5d8eafd6..20a3a7e4 100644 --- a/Shared/Views/PlayerControlsView.swift +++ b/Shared/Views/PlayerControlsView.swift @@ -31,12 +31,12 @@ struct PlayerControlsView: View { }) { HStack { VStack(alignment: .leading, spacing: 3) { - Text(model.currentItem?.video.title ?? "Not playing") + Text(model.currentItem?.video?.title ?? "Not playing") .font(.system(size: 14).bold()) .foregroundColor(model.currentItem.isNil ? .secondary : .accentColor) .lineLimit(1) - Text(model.currentItem?.video.author ?? "Yattee v0.1") + Text(model.currentItem?.video?.author ?? "Yattee v0.1") .fontWeight(model.currentItem.isNil ? .light : .bold) .font(.system(size: 10)) .foregroundColor(.secondary)