Resolution switching support

This commit is contained in:
Arkadiusz Fal
2021-06-14 20:05:02 +02:00
parent 65e5f0f426
commit 4535853ac3
20 changed files with 396 additions and 117 deletions

View File

@@ -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) {}
}

View 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)
}
}
}
}
}

View File

@@ -16,10 +16,10 @@ struct PopularVideosView: View {
}
var videos: [Video] {
if (provider.videos.isEmpty) {
if provider.videos.isEmpty {
provider.load()
}
return provider.videos
}
}

View File

@@ -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
}

View File

@@ -16,6 +16,6 @@ struct SubscriptionsView: View {
}
var videos: [Video] {
return provider.videos
provider.videos
}
}

View File

@@ -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)
// }
// }

View File

@@ -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 {