mirror of
https://github.com/yattee/yattee.git
synced 2025-01-09 14:27:11 +00:00
Better UI handling for loading video details (fixes #46)
This commit is contained in:
parent
0af2db2fd7
commit
89957e3b56
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)")
|
||||||
|
@ -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 = {}) {
|
||||||
|
Loading…
Reference in New Issue
Block a user