Better UI handling for loading video details (fixes #46)

This commit is contained in:
Arkadiusz Fal 2021-12-29 19:55:41 +01:00
parent 0af2db2fd7
commit 89957e3b56
4 changed files with 82 additions and 19 deletions

View File

@ -13,11 +13,13 @@ import SwiftyJSON
final class PlayerModel: ObservableObject { final class PlayerModel: ObservableObject {
static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2] static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2]
static let assetKeysToLoad = ["tracks", "playable", "duration"]
let logger = Logger(label: "stream.yattee.app") let logger = Logger(label: "stream.yattee.app")
private(set) var player = AVPlayer() private(set) var player = AVPlayer()
var playerView = Player() var playerView = Player()
var controller: PlayerViewController? var controller: PlayerViewController?
var playerItem: AVPlayerItem?
@Published var presentingPlayer = false { didSet { handlePresentationChange() } } @Published var presentingPlayer = false { didSet { handlePresentationChange() } }
@ -45,6 +47,7 @@ final class PlayerModel: ObservableObject {
var accounts: AccountsModel var accounts: AccountsModel
var comments: CommentsModel var comments: CommentsModel
var asset: AVURLAsset?
var composition = AVMutableComposition() var composition = AVMutableComposition()
var loadedCompositionAssets = [AVMediaType]() var loadedCompositionAssets = [AVMediaType]()
@ -82,7 +85,6 @@ final class PlayerModel: ObservableObject {
self.accounts = accounts ?? AccountsModel() self.accounts = accounts ?? AccountsModel()
self.comments = comments ?? CommentsModel() self.comments = comments ?? CommentsModel()
addItemDidPlayToEndTimeObserver()
addFrequentTimeObserver() addFrequentTimeObserver()
addInfrequentTimeObserver() addInfrequentTimeObserver()
addPlayerTimeControlStatusObserver() addPlayerTimeControlStatusObserver()
@ -197,7 +199,8 @@ final class PlayerModel: ObservableObject {
if !upgrading { if !upgrading {
resetSegments() resetSegments()
sponsorBlock.loadSegments( DispatchQueue.main.async { [weak self] in
self?.sponsorBlock.loadSegments(
videoID: video.videoID, videoID: video.videoID,
categories: Defaults[.sponsorBlockCategories] categories: Defaults[.sponsorBlockCategories]
) { [weak self] in ) { [weak self] in
@ -206,11 +209,11 @@ final class PlayerModel: ObservableObject {
} }
} }
} }
}
if let url = stream.singleAssetURL { if let url = stream.singleAssetURL {
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)") logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
loadSingleAsset(url, stream: stream, of: video, preservingTime: preservingTime)
insertPlayerItem(stream, for: video, preservingTime: preservingTime)
} else { } else {
logger.info("playing stream with many assets:") logger.info("playing stream with many assets:")
logger.info("composition audio asset: \(stream.audioAsset.url)") logger.info("composition audio asset: \(stream.audioAsset.url)")
@ -282,11 +285,14 @@ final class PlayerModel: ObservableObject {
for video: Video, for video: Video,
preservingTime: Bool = false preservingTime: Bool = false
) { ) {
let playerItem = playerItem(stream) removeItemDidPlayToEndTimeObserver()
playerItem = playerItem(stream)
guard playerItem != nil else { guard playerItem != nil else {
return return
} }
addItemDidPlayToEndTimeObserver()
attachMetadata(to: playerItem!, video: video, for: stream) attachMetadata(to: playerItem!, video: video, for: stream)
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
@ -296,6 +302,7 @@ final class PlayerModel: ObservableObject {
self.stream = stream self.stream = stream
self.composition = AVMutableComposition() self.composition = AVMutableComposition()
self.asset = nil
} }
let startPlaying = { let startPlaying = {
@ -303,7 +310,7 @@ final class PlayerModel: ObservableObject {
try? AVAudioSession.sharedInstance().setActive(true) try? AVAudioSession.sharedInstance().setActive(true)
#endif #endif
if self.isAutoplaying(playerItem!) { if self.isAutoplaying(self.playerItem!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
guard let self = self else { guard let self = self else {
return return
@ -334,7 +341,10 @@ final class PlayerModel: ObservableObject {
} }
let replaceItemAndSeek = { let replaceItemAndSeek = {
self.player.replaceCurrentItem(with: playerItem) guard video == self.currentVideo else {
return
}
self.player.replaceCurrentItem(with: self.playerItem)
self.seekToPreservedTime { finished in self.seekToPreservedTime { finished in
guard finished else { guard finished else {
return return
@ -361,6 +371,30 @@ final class PlayerModel: ObservableObject {
} }
} }
private func loadSingleAsset(
_ url: URL,
stream: Stream,
of video: Video,
preservingTime: Bool = false
) {
asset?.cancelLoading()
asset = AVURLAsset(url: url)
asset?.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
var error: NSError?
switch self?.asset?.statusOfValue(forKey: "duration", error: &error) {
case .loaded:
DispatchQueue.main.async { [weak self] in
self?.insertPlayerItem(stream, for: video, preservingTime: preservingTime)
}
case .failed:
self?.playerError = error
default:
return
}
}
}
private func loadComposition( private func loadComposition(
_ stream: Stream, _ stream: Stream,
of video: Video, of video: Video,
@ -378,7 +412,7 @@ final class PlayerModel: ObservableObject {
of video: Video, of video: Video,
preservingTime: Bool = false preservingTime: Bool = false
) { ) {
asset.loadValuesAsynchronously(forKeys: ["playable"]) { [weak self] in asset.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
guard let self = self else { guard let self = self else {
return return
} }
@ -420,9 +454,9 @@ final class PlayerModel: ObservableObject {
} }
} }
private func playerItem(_ stream: Stream) -> AVPlayerItem? { private func playerItem(_: Stream) -> AVPlayerItem? {
if let url = stream.singleAssetURL { if let asset = asset {
return AVPlayerItem(asset: AVURLAsset(url: url)) return AVPlayerItem(asset: asset)
} else { } else {
return AVPlayerItem(asset: composition) return AVPlayerItem(asset: composition)
} }
@ -489,7 +523,15 @@ final class PlayerModel: ObservableObject {
self, self,
selector: #selector(itemDidPlayToEndTime), selector: #selector(itemDidPlayToEndTime),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: nil object: playerItem
)
}
private func removeItemDidPlayToEndTimeObserver() {
NotificationCenter.default.removeObserver(
self,
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: playerItem
) )
} }

View File

@ -62,7 +62,13 @@ extension PlayerModel {
preservedTime = currentItem.playbackTime preservedTime = currentItem.playbackTime
restoreLoadedChannel() restoreLoadedChannel()
loadAvailableStreams(currentVideo!) DispatchQueue.main.async { [weak self] in
guard let video = self?.currentVideo else {
return
}
self?.loadAvailableStreams(video)
}
} }
func preferredStream(_ streams: [Stream]) -> Stream? { func preferredStream(_ streams: [Stream]) -> Stream? {
@ -95,6 +101,9 @@ extension PlayerModel {
remove(newItem) remove(newItem)
currentItem = newItem
player.pause()
accounts.api.loadDetails(newItem) { newItem in accounts.api.loadDetails(newItem) { newItem in
self.playItem(newItem, video: newItem.video, at: time) self.playItem(newItem, video: newItem.video, at: time)
} }
@ -135,6 +144,12 @@ extension PlayerModel {
) -> PlayerQueueItem? { ) -> PlayerQueueItem? {
let item = PlayerQueueItem(video, playbackTime: atTime) let item = PlayerQueueItem(video, playbackTime: atTime)
if play {
currentItem = item
// pause playing current video as it's going to be replaced with next one
player.pause()
}
queue.insert(item, at: prepending ? 0 : queue.endIndex) queue.insert(item, at: prepending ? 0 : queue.endIndex)
accounts.api.loadDetails(item) { newItem in accounts.api.loadDetails(item) { newItem in

View File

@ -43,6 +43,10 @@ extension PlayerModel {
.load() .load()
.onSuccess { response in .onSuccess { response in
if let video: Video = response.typedContent() { if let video: Video = response.typedContent() {
guard video == self.currentVideo else {
self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed")
return
}
self.availableStreams += self.streamsWithInstance(instance: instance, streams: video.streams) self.availableStreams += self.streamsWithInstance(instance: instance, streams: video.streams)
} else { } else {
self.logger.critical("no streams available from \(instance.description)") self.logger.critical("no streams available from \(instance.description)")

View File

@ -35,7 +35,9 @@ final class SponsorBlockAPI: ObservableObject {
self.videoID = videoID self.videoID = videoID
requestSegments(categories: categories, completionHandler: completionHandler) DispatchQueue.main.async { [weak self] in
self?.requestSegments(categories: categories, completionHandler: completionHandler)
}
} }
private func requestSegments(categories: Set<String>, completionHandler: @escaping () -> Void = {}) { private func requestSegments(categories: Set<String>, completionHandler: @escaping () -> Void = {}) {