mirror of
https://github.com/yattee/yattee.git
synced 2025-10-10 01:18:16 +00:00
Resolution switching support
This commit is contained in:
@@ -5,24 +5,15 @@ import SwiftUI
|
||||
struct PlayerView: View {
|
||||
@ObservedObject private var provider: VideoDetailsProvider
|
||||
|
||||
private var id: String
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
provider = VideoDetailsProvider(id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let video = provider.video {
|
||||
if video.url != nil {
|
||||
PlayerViewController(video)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
if video.error {
|
||||
Text("Video can not be loaded")
|
||||
}
|
||||
PlayerViewController(video: video)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
@@ -32,37 +23,3 @@ struct PlayerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerViewController: UIViewControllerRepresentable {
|
||||
var video: Video
|
||||
|
||||
init(_ video: Video) {
|
||||
self.video = video
|
||||
}
|
||||
|
||||
private var player: AVPlayer {
|
||||
let item = AVPlayerItem(url: video.url!)
|
||||
item.externalMetadata = [makeMetadataItem(.commonIdentifierTitle, value: video.title)]
|
||||
|
||||
return AVPlayer(playerItem: item)
|
||||
}
|
||||
|
||||
private func makeMetadataItem(_ identifier: AVMetadataIdentifier, value: Any) -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
item.extendedLanguageTag = "und"
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
|
||||
func makeUIViewController(context _: Context) -> AVPlayerViewController {
|
||||
let controller = AVPlayerViewController()
|
||||
controller.modalPresentationStyle = .fullScreen
|
||||
controller.player = player
|
||||
controller.title = video.title
|
||||
controller.player?.play()
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_: AVPlayerViewController, context _: Context) {}
|
||||
}
|
||||
|
142
Apple TV/PlayerViewController.swift
Normal file
142
Apple TV/PlayerViewController.swift
Normal file
@@ -0,0 +1,142 @@
|
||||
import AVKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerViewController: UIViewControllerRepresentable {
|
||||
@ObservedObject private var state = PlayerState()
|
||||
@ObservedObject var video: Video
|
||||
|
||||
var player = AVPlayer()
|
||||
var composition = AVMutableComposition()
|
||||
|
||||
var audioTrack: AVMutableCompositionTrack {
|
||||
composition.tracks(withMediaType: .audio).first ?? composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!
|
||||
}
|
||||
|
||||
var videoTrack: AVMutableCompositionTrack {
|
||||
composition.tracks(withMediaType: .video).first ?? composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!
|
||||
}
|
||||
|
||||
var playerItem: AVPlayerItem {
|
||||
let playerItem = AVPlayerItem(asset: composition)
|
||||
|
||||
playerItem.externalMetadata = [makeMetadataItem(.commonIdentifierTitle, value: video.title)]
|
||||
|
||||
return playerItem
|
||||
}
|
||||
|
||||
init(video: Video) {
|
||||
self.video = video
|
||||
state.currentStream = video.defaultStream
|
||||
|
||||
addTracksAndLoadAssets(state.currentStream!)
|
||||
}
|
||||
|
||||
func addTracksAndLoadAssets(_ stream: Stream) {
|
||||
composition.removeTrack(audioTrack)
|
||||
composition.removeTrack(videoTrack)
|
||||
|
||||
let keys = ["playable"]
|
||||
|
||||
stream.audioAsset.loadValuesAsynchronously(forKeys: keys) {
|
||||
DispatchQueue.main.async {
|
||||
guard let track = stream.audioAsset.tracks(withMediaType: .audio).first else {
|
||||
return
|
||||
}
|
||||
|
||||
try? audioTrack.insertTimeRange(
|
||||
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1)),
|
||||
of: track,
|
||||
at: .zero
|
||||
)
|
||||
|
||||
handleAssetLoad(stream)
|
||||
}
|
||||
}
|
||||
|
||||
stream.videoAsset.loadValuesAsynchronously(forKeys: keys) {
|
||||
DispatchQueue.main.async {
|
||||
guard let track = stream.videoAsset.tracks(withMediaType: .video).first else {
|
||||
return
|
||||
}
|
||||
|
||||
try? videoTrack.insertTimeRange(
|
||||
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1)),
|
||||
of: track,
|
||||
at: .zero
|
||||
)
|
||||
|
||||
handleAssetLoad(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAssetLoad(_ stream: Stream) {
|
||||
var error: NSError?
|
||||
let status = stream.videoAsset.statusOfValue(forKey: "playable", error: &error)
|
||||
|
||||
switch status {
|
||||
case .loaded:
|
||||
let resumeAt = player.currentTime()
|
||||
|
||||
if resumeAt.seconds > 0 {
|
||||
state.seekTo = resumeAt
|
||||
}
|
||||
|
||||
state.currentStream = stream
|
||||
|
||||
player.replaceCurrentItem(with: playerItem)
|
||||
|
||||
if let time = state.seekTo {
|
||||
player.seek(to: time)
|
||||
}
|
||||
|
||||
player.play()
|
||||
|
||||
default:
|
||||
if error != nil {
|
||||
print("loading error: \(error!)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeMetadataItem(_ identifier: AVMetadataIdentifier, value: Any) -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
item.extendedLanguageTag = "und"
|
||||
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
|
||||
func makeUIViewController(context _: Context) -> AVPlayerViewController {
|
||||
let controller = AVPlayerViewController()
|
||||
|
||||
controller.transportBarCustomMenuItems = [streamingQualityMenu]
|
||||
controller.modalPresentationStyle = .fullScreen
|
||||
controller.player = player
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ controller: AVPlayerViewController, context _: Context) {
|
||||
controller.transportBarCustomMenuItems = [streamingQualityMenu]
|
||||
}
|
||||
|
||||
var streamingQualityMenu: UIMenu {
|
||||
UIMenu(title: "Streaming quality", image: UIImage(systemName: "4k.tv"), children: streamingQualityMenuActions)
|
||||
}
|
||||
|
||||
var streamingQualityMenuActions: [UIAction] {
|
||||
video.selectableStreams.map { stream in
|
||||
let image = self.state.currentStream == stream ? UIImage(systemName: "checkmark") : nil
|
||||
|
||||
return UIAction(title: stream.description, image: image) { _ in
|
||||
DispatchQueue.main.async {
|
||||
addTracksAndLoadAssets(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,10 +16,10 @@ struct PopularVideosView: View {
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
if (provider.videos.isEmpty) {
|
||||
if provider.videos.isEmpty {
|
||||
provider.load()
|
||||
}
|
||||
|
||||
|
||||
return provider.videos
|
||||
}
|
||||
}
|
||||
|
@@ -14,19 +14,7 @@ struct SearchView: View {
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
var newQuery = query
|
||||
|
||||
if let url = URLComponents(string: query),
|
||||
let queryItem = url.queryItems?.first(where: { item in item.name == "v" }),
|
||||
let id = queryItem.value
|
||||
{
|
||||
newQuery = id
|
||||
}
|
||||
|
||||
if newQuery != provider.query {
|
||||
provider.query = newQuery
|
||||
provider.load()
|
||||
}
|
||||
provider.load(query)
|
||||
|
||||
return provider.videos
|
||||
}
|
||||
|
@@ -16,6 +16,6 @@ struct SubscriptionsView: View {
|
||||
}
|
||||
|
||||
var videos: [Video] {
|
||||
return provider.videos
|
||||
provider.videos
|
||||
}
|
||||
}
|
||||
|
@@ -67,16 +67,16 @@ struct VideoThumbnailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct VideoThumbnailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideoThumbnailView(video: Video(
|
||||
id: "A",
|
||||
title: "A very very long text which",
|
||||
thumbnailURL: URL(string: "https://invidious.home.arekf.net/vi/yXohcxCKqvo/maxres.jpg")!,
|
||||
author: "Bear",
|
||||
length: 240,
|
||||
published: "2 days ago",
|
||||
channelID: ""
|
||||
)).frame(maxWidth: 350)
|
||||
}
|
||||
}
|
||||
// struct VideoThumbnailView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// VideoThumbnailView(video: Video(
|
||||
// id: "A",
|
||||
// title: "A very very long text which",
|
||||
// thumbnailURL: URL(string: "https://invidious.home.arekf.net/vi/yXohcxCKqvo/maxres.jpg")!,
|
||||
// author: "Bear",
|
||||
// length: 240,
|
||||
// published: "2 days ago",
|
||||
// channelID: ""
|
||||
// )).frame(maxWidth: 350)
|
||||
// }
|
||||
// }
|
||||
|
@@ -27,17 +27,17 @@ struct VideosView: View {
|
||||
}
|
||||
|
||||
func openChannelButton(from video: Video) -> some View {
|
||||
Button("\(video.author) Channel", action: {
|
||||
Button("\(video.author) Channel") {
|
||||
state.openChannel(from: video)
|
||||
tabSelection = .channel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func closeChannelButton(name: String) -> some View {
|
||||
Button("Close \(name) Channel", action: {
|
||||
Button("Close \(name) Channel") {
|
||||
tabSelection = .popular
|
||||
state.closeChannel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var listRowInsets: EdgeInsets {
|
||||
|
Reference in New Issue
Block a user