mirror of
https://github.com/yattee/yattee.git
synced 2024-11-10 00:08:21 +00:00
Resolution switching support
This commit is contained in:
parent
65e5f0f426
commit
4535853ac3
@ -5,25 +5,16 @@ import SwiftUI
|
|||||||
struct PlayerView: View {
|
struct PlayerView: View {
|
||||||
@ObservedObject private var provider: VideoDetailsProvider
|
@ObservedObject private var provider: VideoDetailsProvider
|
||||||
|
|
||||||
private var id: String
|
|
||||||
|
|
||||||
init(id: String) {
|
init(id: String) {
|
||||||
self.id = id
|
|
||||||
provider = VideoDetailsProvider(id)
|
provider = VideoDetailsProvider(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if let video = provider.video {
|
if let video = provider.video {
|
||||||
if video.url != nil {
|
PlayerViewController(video: video)
|
||||||
PlayerViewController(video)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
}
|
}
|
||||||
|
|
||||||
if video.error {
|
|
||||||
Text("Video can not be loaded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
async {
|
async {
|
||||||
@ -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,7 +16,7 @@ struct PopularVideosView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var videos: [Video] {
|
var videos: [Video] {
|
||||||
if (provider.videos.isEmpty) {
|
if provider.videos.isEmpty {
|
||||||
provider.load()
|
provider.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,19 +14,7 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var videos: [Video] {
|
var videos: [Video] {
|
||||||
var newQuery = query
|
provider.load(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.videos
|
return provider.videos
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,6 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var videos: [Video] {
|
var videos: [Video] {
|
||||||
return provider.videos
|
provider.videos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,16 +67,16 @@ struct VideoThumbnailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoThumbnailView_Previews: PreviewProvider {
|
// struct VideoThumbnailView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
// static var previews: some View {
|
||||||
VideoThumbnailView(video: Video(
|
// VideoThumbnailView(video: Video(
|
||||||
id: "A",
|
// id: "A",
|
||||||
title: "A very very long text which",
|
// title: "A very very long text which",
|
||||||
thumbnailURL: URL(string: "https://invidious.home.arekf.net/vi/yXohcxCKqvo/maxres.jpg")!,
|
// thumbnailURL: URL(string: "https://invidious.home.arekf.net/vi/yXohcxCKqvo/maxres.jpg")!,
|
||||||
author: "Bear",
|
// author: "Bear",
|
||||||
length: 240,
|
// length: 240,
|
||||||
published: "2 days ago",
|
// published: "2 days ago",
|
||||||
channelID: ""
|
// channelID: ""
|
||||||
)).frame(maxWidth: 350)
|
// )).frame(maxWidth: 350)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
@ -27,17 +27,17 @@ struct VideosView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openChannelButton(from video: Video) -> some View {
|
func openChannelButton(from video: Video) -> some View {
|
||||||
Button("\(video.author) Channel", action: {
|
Button("\(video.author) Channel") {
|
||||||
state.openChannel(from: video)
|
state.openChannel(from: video)
|
||||||
tabSelection = .channel
|
tabSelection = .channel
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeChannelButton(name: String) -> some View {
|
func closeChannelButton(name: String) -> some View {
|
||||||
Button("Close \(name) Channel", action: {
|
Button("Close \(name) Channel") {
|
||||||
tabSelection = .popular
|
tabSelection = .popular
|
||||||
state.closeChannel()
|
state.closeChannel()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var listRowInsets: EdgeInsets {
|
var listRowInsets: EdgeInsets {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import AVFoundation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class AppState: ObservableObject {
|
final class AppState: ObservableObject {
|
||||||
@Published var showingChannel = false
|
@Published var showingChannel = false
|
||||||
@Published var channelID: String = ""
|
@Published var channelID: String = ""
|
||||||
@Published var channel: String = ""
|
@Published var channel: String = ""
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
class ChannelVideosProvider: DataProvider {
|
final class ChannelVideosProvider: DataProvider {
|
||||||
@Published var videos = [Video]()
|
@Published var videos = [Video]()
|
||||||
|
|
||||||
var channelID: String? = ""
|
var channelID: String? = ""
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
guard channelID != nil else { return }
|
guard channelID != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let searchPath = "channels/\(channelID!)"
|
let searchPath = "channels/\(channelID!)"
|
||||||
DataProvider.request(searchPath).responseJSON { response in
|
DataProvider.request(searchPath).responseJSON { response in
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
import Alamofire
|
import Alamofire
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// swiftlint:disable:next final_class
|
||||||
class DataProvider: ObservableObject {
|
class DataProvider: ObservableObject {
|
||||||
static let instance = "https://invidious.home.arekf.net"
|
static let instance = "https://invidious.home.arekf.net"
|
||||||
|
|
||||||
|
static func proxyURLForAsset(_ url: String) -> URL? {
|
||||||
|
guard let instanceURLComponents = URLComponents(string: DataProvider.instance),
|
||||||
|
var urlComponents = URLComponents(string: url) else { return nil }
|
||||||
|
|
||||||
|
urlComponents.scheme = instanceURLComponents.scheme
|
||||||
|
urlComponents.host = instanceURLComponents.host
|
||||||
|
|
||||||
|
return urlComponents.url
|
||||||
|
}
|
||||||
|
|
||||||
static func request(_ path: String, headers: HTTPHeaders? = nil) -> DataRequest {
|
static func request(_ path: String, headers: HTTPHeaders? = nil) -> DataRequest {
|
||||||
AF.request(apiURLString(path), headers: headers)
|
AF.request(apiURLString(path), headers: headers)
|
||||||
}
|
}
|
||||||
|
12
Model/MuxedStream.swift
Normal file
12
Model/MuxedStream.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class MuxedStream: Stream {
|
||||||
|
var muxedAsset: AVURLAsset
|
||||||
|
|
||||||
|
init(muxedAsset: AVURLAsset, resolution: StreamResolution, type: StreamType, encoding: String) {
|
||||||
|
self.muxedAsset = muxedAsset
|
||||||
|
|
||||||
|
super.init(audioAsset: muxedAsset, videoAsset: muxedAsset, resolution: resolution, type: type, encoding: encoding)
|
||||||
|
}
|
||||||
|
}
|
7
Model/PlayerState.swift
Normal file
7
Model/PlayerState.swift
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class PlayerState: ObservableObject {
|
||||||
|
@Published var currentStream: Stream!
|
||||||
|
@Published var seekTo: CMTime?
|
||||||
|
}
|
@ -1,13 +1,28 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
class SearchedVideosProvider: DataProvider {
|
final class SearchedVideosProvider: DataProvider {
|
||||||
@Published var videos = [Video]()
|
@Published var videos = [Video]()
|
||||||
|
|
||||||
var query: String = ""
|
var currentQuery: String = ""
|
||||||
|
|
||||||
func load() {
|
func load(_ query: String) {
|
||||||
let searchPath = "search?q=\(query.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!)"
|
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 == currentQuery {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentQuery = newQuery
|
||||||
|
|
||||||
|
let searchPath = "search?q=\(currentQuery.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!)"
|
||||||
DataProvider.request(searchPath).responseJSON { response in
|
DataProvider.request(searchPath).responseJSON { response in
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case let .success(value):
|
case let .success(value):
|
||||||
|
29
Model/Stream.swift
Normal file
29
Model/Stream.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// swiftlint:disable:next final_class
|
||||||
|
class Stream: Equatable {
|
||||||
|
var audioAsset: AVURLAsset
|
||||||
|
var videoAsset: AVURLAsset
|
||||||
|
|
||||||
|
var resolution: StreamResolution
|
||||||
|
var type: StreamType
|
||||||
|
|
||||||
|
var encoding: String
|
||||||
|
|
||||||
|
init(audioAsset: AVURLAsset, videoAsset: AVURLAsset, resolution: StreamResolution, type: StreamType, encoding: String) {
|
||||||
|
self.audioAsset = audioAsset
|
||||||
|
self.videoAsset = videoAsset
|
||||||
|
self.resolution = resolution
|
||||||
|
self.type = type
|
||||||
|
self.encoding = encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
"\(resolution.height)p"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: Stream, rhs: Stream) -> Bool {
|
||||||
|
lhs.resolution == rhs.resolution && lhs.type == rhs.type
|
||||||
|
}
|
||||||
|
}
|
17
Model/StreamResolution.swift
Normal file
17
Model/StreamResolution.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum StreamResolution: String, CaseIterable, Comparable {
|
||||||
|
case hd_1080p, hd_720p, sd_480p, sd_360p, sd_240p, sd_144p
|
||||||
|
|
||||||
|
var height: Int {
|
||||||
|
Int(rawValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from(resolution: String) -> StreamResolution? {
|
||||||
|
allCases.first { "\($0)".contains(resolution) }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: StreamResolution, rhs: StreamResolution) -> Bool {
|
||||||
|
lhs.height < rhs.height
|
||||||
|
}
|
||||||
|
}
|
18
Model/StreamType.swift
Normal file
18
Model/StreamType.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum StreamType: String, Comparable {
|
||||||
|
case stream, adaptive
|
||||||
|
|
||||||
|
private var sortOrder: Int {
|
||||||
|
switch self {
|
||||||
|
case .stream:
|
||||||
|
return 0
|
||||||
|
case .adaptive:
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: StreamType, rhs: StreamType) -> Bool {
|
||||||
|
lhs.sortOrder < rhs.sortOrder
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import Alamofire
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
class SubscriptionVideosProvider: DataProvider {
|
final class SubscriptionVideosProvider: DataProvider {
|
||||||
@Published var videos = [Video]()
|
@Published var videos = [Video]()
|
||||||
|
|
||||||
var sid: String = "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="
|
var sid: String = "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Alamofire
|
import Alamofire
|
||||||
|
import AVKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
@ -10,11 +11,9 @@ final class Video: Identifiable, ObservableObject {
|
|||||||
var length: TimeInterval
|
var length: TimeInterval
|
||||||
var published: String
|
var published: String
|
||||||
var views: Int
|
var views: Int
|
||||||
|
|
||||||
var channelID: String
|
var channelID: String
|
||||||
|
|
||||||
@Published var url: URL?
|
var streams = [Stream]()
|
||||||
@Published var error: Bool = false
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String,
|
id: String,
|
||||||
@ -23,8 +22,8 @@ final class Video: Identifiable, ObservableObject {
|
|||||||
author: String,
|
author: String,
|
||||||
length: TimeInterval,
|
length: TimeInterval,
|
||||||
published: String,
|
published: String,
|
||||||
channelID: String,
|
views: Int,
|
||||||
views: Int = 0
|
channelID: String
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -32,41 +31,22 @@ final class Video: Identifiable, ObservableObject {
|
|||||||
self.author = author
|
self.author = author
|
||||||
self.length = length
|
self.length = length
|
||||||
self.published = published
|
self.published = published
|
||||||
self.channelID = channelID
|
|
||||||
self.views = views
|
self.views = views
|
||||||
|
self.channelID = channelID
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ json: JSON) {
|
init(_ json: JSON) {
|
||||||
func extractThumbnailURL(from details: JSON) -> URL? {
|
|
||||||
if details["videoThumbnails"].arrayValue.isEmpty {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let thumbnail = details["videoThumbnails"].arrayValue.first(where: { $0["quality"].stringValue == "medium" })!
|
|
||||||
return thumbnail["url"].url!
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractFormatStreamURL(from streams: [JSON]) -> URL? {
|
|
||||||
if streams.isEmpty {
|
|
||||||
error = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let stream = streams.last!
|
|
||||||
|
|
||||||
return stream["url"].url
|
|
||||||
}
|
|
||||||
|
|
||||||
id = json["videoId"].stringValue
|
id = json["videoId"].stringValue
|
||||||
title = json["title"].stringValue
|
title = json["title"].stringValue
|
||||||
thumbnailURL = extractThumbnailURL(from: json)
|
|
||||||
author = json["author"].stringValue
|
author = json["author"].stringValue
|
||||||
length = json["lengthSeconds"].doubleValue
|
length = json["lengthSeconds"].doubleValue
|
||||||
published = json["publishedText"].stringValue
|
published = json["publishedText"].stringValue
|
||||||
views = json["viewCount"].intValue
|
views = json["viewCount"].intValue
|
||||||
channelID = json["authorId"].stringValue
|
channelID = json["authorId"].stringValue
|
||||||
|
thumbnailURL = extractThumbnailURL(from: json)
|
||||||
|
|
||||||
url = extractFormatStreamURL(from: json["formatStreams"].arrayValue)
|
streams = extractFormatStreams(from: json["formatStreams"].arrayValue)
|
||||||
|
streams.append(contentsOf: extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
var playTime: String? {
|
var playTime: String? {
|
||||||
@ -97,4 +77,60 @@ final class Video: Identifiable, ObservableObject {
|
|||||||
|
|
||||||
return "\(formatter.string(from: number)!)\(unit)"
|
return "\(formatter.string(from: number)!)\(unit)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var selectableStreams: [Stream] {
|
||||||
|
let streams = streams.sorted { $0.resolution > $1.resolution }
|
||||||
|
var selectable = [Stream]()
|
||||||
|
|
||||||
|
StreamResolution.allCases.forEach { resolution in
|
||||||
|
if let stream = streams.filter({ $0.resolution == resolution }).min(by: { $0.type < $1.type }) {
|
||||||
|
selectable.append(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectable
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultStream: Stream? {
|
||||||
|
selectableStreams.first { $0.type == .stream }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractThumbnailURL(from details: JSON) -> URL? {
|
||||||
|
if details["videoThumbnails"].arrayValue.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumbnail = details["videoThumbnails"].arrayValue.first { $0["quality"].stringValue == "medium" }!
|
||||||
|
return thumbnail["url"].url!
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
||||||
|
streams.map {
|
||||||
|
MuxedStream(
|
||||||
|
muxedAsset: AVURLAsset(url: DataProvider.proxyURLForAsset($0["url"].stringValue)!),
|
||||||
|
resolution: StreamResolution.from(resolution: $0["resolution"].stringValue)!,
|
||||||
|
type: .stream,
|
||||||
|
encoding: $0["encoding"].stringValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] {
|
||||||
|
let audioAssetURL = streams.first { $0["type"].stringValue.starts(with: "audio/mp4") }
|
||||||
|
guard audioAssetURL != nil else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoAssetsURLs = streams.filter { $0["type"].stringValue.starts(with: "video/mp4") && $0["encoding"].stringValue == "h264" }
|
||||||
|
|
||||||
|
return videoAssetsURLs.map {
|
||||||
|
Stream(
|
||||||
|
audioAsset: AVURLAsset(url: DataProvider.proxyURLForAsset(audioAssetURL!["url"].stringValue)!),
|
||||||
|
videoAsset: AVURLAsset(url: DataProvider.proxyURLForAsset($0["url"].stringValue)!),
|
||||||
|
resolution: StreamResolution.from(resolution: $0["resolution"].stringValue)!,
|
||||||
|
type: .adaptive,
|
||||||
|
encoding: $0["encoding"].stringValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741B52F2676213400125C5E /* PlayerViewController.swift */; };
|
||||||
37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularVideosView.swift */; };
|
37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularVideosView.swift */; };
|
||||||
37AAF28026737550007FC770 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
|
37AAF28026737550007FC770 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
|
||||||
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */; };
|
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */; };
|
||||||
@ -29,6 +30,21 @@
|
|||||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */; };
|
||||||
|
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||||
|
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||||
|
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||||
|
37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B42677B628005A1EFE /* StreamType.swift */; };
|
||||||
|
37CEE4B62677B628005A1EFE /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B42677B628005A1EFE /* StreamType.swift */; };
|
||||||
|
37CEE4B72677B628005A1EFE /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B42677B628005A1EFE /* StreamType.swift */; };
|
||||||
|
37CEE4B92677B63F005A1EFE /* StreamResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B82677B63F005A1EFE /* StreamResolution.swift */; };
|
||||||
|
37CEE4BA2677B63F005A1EFE /* StreamResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B82677B63F005A1EFE /* StreamResolution.swift */; };
|
||||||
|
37CEE4BB2677B63F005A1EFE /* StreamResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4B82677B63F005A1EFE /* StreamResolution.swift */; };
|
||||||
|
37CEE4BD2677B670005A1EFE /* MuxedStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* MuxedStream.swift */; };
|
||||||
|
37CEE4BE2677B670005A1EFE /* MuxedStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* MuxedStream.swift */; };
|
||||||
|
37CEE4BF2677B670005A1EFE /* MuxedStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4BC2677B670005A1EFE /* MuxedStream.swift */; };
|
||||||
|
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4C02677B697005A1EFE /* Stream.swift */; };
|
||||||
|
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4C02677B697005A1EFE /* Stream.swift */; };
|
||||||
|
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEE4C02677B697005A1EFE /* Stream.swift */; };
|
||||||
37D4B0D92671614900C925CA /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0D82671614900C925CA /* Tests_iOS.swift */; };
|
37D4B0D92671614900C925CA /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0D82671614900C925CA /* Tests_iOS.swift */; };
|
||||||
37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0E22671614900C925CA /* Tests_macOS.swift */; };
|
37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0E22671614900C925CA /* Tests_macOS.swift */; };
|
||||||
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C22671614700C925CA /* PearvidiousApp.swift */; };
|
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C22671614700C925CA /* PearvidiousApp.swift */; };
|
||||||
@ -87,6 +103,7 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
3741B52F2676213400125C5E /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
|
||||||
37AAF27D26737323007FC770 /* PopularVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularVideosView.swift; sourceTree = "<group>"; };
|
37AAF27D26737323007FC770 /* PopularVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularVideosView.swift; sourceTree = "<group>"; };
|
||||||
37AAF27F26737550007FC770 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
37AAF27F26737550007FC770 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||||
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedVideosProvider.swift; sourceTree = "<group>"; };
|
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedVideosProvider.swift; sourceTree = "<group>"; };
|
||||||
@ -97,6 +114,11 @@
|
|||||||
37AAF29926740A01007FC770 /* VideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosView.swift; sourceTree = "<group>"; };
|
37AAF29926740A01007FC770 /* VideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosView.swift; sourceTree = "<group>"; };
|
||||||
37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionVideosProvider.swift; sourceTree = "<group>"; };
|
37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionVideosProvider.swift; sourceTree = "<group>"; };
|
||||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
|
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
|
||||||
|
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
|
||||||
|
37CEE4B42677B628005A1EFE /* StreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamType.swift; sourceTree = "<group>"; };
|
||||||
|
37CEE4B82677B63F005A1EFE /* StreamResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamResolution.swift; sourceTree = "<group>"; };
|
||||||
|
37CEE4BC2677B670005A1EFE /* MuxedStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuxedStream.swift; sourceTree = "<group>"; };
|
||||||
|
37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = "<group>"; };
|
||||||
37D4B0C22671614700C925CA /* PearvidiousApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousApp.swift; sourceTree = "<group>"; };
|
37D4B0C22671614700C925CA /* PearvidiousApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousApp.swift; sourceTree = "<group>"; };
|
||||||
37D4B0C32671614700C925CA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
37D4B0C32671614700C925CA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
37D4B0C42671614800C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
37D4B0C42671614800C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@ -230,6 +252,7 @@
|
|||||||
37AAF2892673AB89007FC770 /* ChannelView.swift */,
|
37AAF2892673AB89007FC770 /* ChannelView.swift */,
|
||||||
37AAF27F26737550007FC770 /* SearchView.swift */,
|
37AAF27F26737550007FC770 /* SearchView.swift */,
|
||||||
37D4B1822671681B00C925CA /* PlayerView.swift */,
|
37D4B1822671681B00C925CA /* PlayerView.swift */,
|
||||||
|
3741B52F2676213400125C5E /* PlayerViewController.swift */,
|
||||||
37AAF29926740A01007FC770 /* VideosView.swift */,
|
37AAF29926740A01007FC770 /* VideosView.swift */,
|
||||||
37D4B18B26717B3800C925CA /* VideoThumbnailView.swift */,
|
37D4B18B26717B3800C925CA /* VideoThumbnailView.swift */,
|
||||||
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
||||||
@ -250,13 +273,18 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
37AAF28F26740715007FC770 /* AppState.swift */,
|
37AAF28F26740715007FC770 /* AppState.swift */,
|
||||||
37D4B19626717E1500C925CA /* Video.swift */,
|
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
|
||||||
37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */,
|
37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */,
|
||||||
37D4B19226717CE100C925CA /* PopularVideosProvider.swift */,
|
37D4B19226717CE100C925CA /* PopularVideosProvider.swift */,
|
||||||
37AAF28B2673ABD3007FC770 /* ChannelVideosProvider.swift */,
|
37AAF28B2673ABD3007FC770 /* ChannelVideosProvider.swift */,
|
||||||
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */,
|
37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */,
|
||||||
37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */,
|
37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */,
|
||||||
37D4B1AF2672A01000C925CA /* DataProvider.swift */,
|
37D4B1AF2672A01000C925CA /* DataProvider.swift */,
|
||||||
|
37D4B19626717E1500C925CA /* Video.swift */,
|
||||||
|
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
||||||
|
37CEE4B42677B628005A1EFE /* StreamType.swift */,
|
||||||
|
37CEE4B82677B63F005A1EFE /* StreamResolution.swift */,
|
||||||
|
37CEE4BC2677B670005A1EFE /* MuxedStream.swift */,
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -490,18 +518,23 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
37CEE4BD2677B670005A1EFE /* MuxedStream.swift in Sources */,
|
||||||
37D4B19326717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
37D4B19326717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
||||||
37AAF29C26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
37AAF29C26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
||||||
|
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
37D4B0E62671614900C925CA /* ContentView.swift in Sources */,
|
37D4B0E62671614900C925CA /* ContentView.swift in Sources */,
|
||||||
|
37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */,
|
||||||
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
37AAF2822673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
||||||
37AAF29026740715007FC770 /* AppState.swift in Sources */,
|
37AAF29026740715007FC770 /* AppState.swift in Sources */,
|
||||||
37AAF2942674086B007FC770 /* TabSelection.swift in Sources */,
|
37AAF2942674086B007FC770 /* TabSelection.swift in Sources */,
|
||||||
37D4B1B02672A01000C925CA /* DataProvider.swift in Sources */,
|
37D4B1B02672A01000C925CA /* DataProvider.swift in Sources */,
|
||||||
37AAF28C2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
37AAF28C2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
||||||
|
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||||
37D4B1B42672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
37D4B1B42672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
||||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
||||||
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
|
37CEE4B92677B63F005A1EFE /* StreamResolution.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -509,18 +542,23 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
37CEE4BE2677B670005A1EFE /* MuxedStream.swift in Sources */,
|
||||||
37D4B19426717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
37D4B19426717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
||||||
37AAF29D26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
37AAF29D26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
||||||
|
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
37D4B0E72671614900C925CA /* ContentView.swift in Sources */,
|
37D4B0E72671614900C925CA /* ContentView.swift in Sources */,
|
||||||
|
37CEE4B62677B628005A1EFE /* StreamType.swift in Sources */,
|
||||||
37AAF2832673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
37AAF2832673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
||||||
37AAF29126740715007FC770 /* AppState.swift in Sources */,
|
37AAF29126740715007FC770 /* AppState.swift in Sources */,
|
||||||
37AAF2952674086B007FC770 /* TabSelection.swift in Sources */,
|
37AAF2952674086B007FC770 /* TabSelection.swift in Sources */,
|
||||||
37D4B1B12672A01000C925CA /* DataProvider.swift in Sources */,
|
37D4B1B12672A01000C925CA /* DataProvider.swift in Sources */,
|
||||||
37AAF28D2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
37AAF28D2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
||||||
|
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||||
37D4B1B52672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
37D4B1B52672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
||||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||||
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
|
37CEE4BA2677B63F005A1EFE /* StreamResolution.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -545,23 +583,29 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
37AAF28026737550007FC770 /* SearchView.swift in Sources */,
|
37AAF28026737550007FC770 /* SearchView.swift in Sources */,
|
||||||
|
37CEE4BF2677B670005A1EFE /* MuxedStream.swift in Sources */,
|
||||||
|
37CEE4B72677B628005A1EFE /* StreamType.swift in Sources */,
|
||||||
37D4B19526717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
37D4B19526717CE100C925CA /* PopularVideosProvider.swift in Sources */,
|
||||||
37AAF29E26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
37AAF29E26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
|
||||||
37D4B1842671684E00C925CA /* PlayerView.swift in Sources */,
|
37D4B1842671684E00C925CA /* PlayerView.swift in Sources */,
|
||||||
37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
37D4B1B22672A01000C925CA /* DataProvider.swift in Sources */,
|
37D4B1B22672A01000C925CA /* DataProvider.swift in Sources */,
|
||||||
37AAF29226740715007FC770 /* AppState.swift in Sources */,
|
37AAF29226740715007FC770 /* AppState.swift in Sources */,
|
||||||
|
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */,
|
||||||
|
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||||
37D4B18E26717B3800C925CA /* VideoThumbnailView.swift in Sources */,
|
37D4B18E26717B3800C925CA /* VideoThumbnailView.swift in Sources */,
|
||||||
37D4B1B62672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
37D4B1B62672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
|
||||||
37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */,
|
37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */,
|
||||||
37AAF29A26740A01007FC770 /* VideosView.swift in Sources */,
|
37AAF29A26740A01007FC770 /* VideosView.swift in Sources */,
|
||||||
37AAF2962674086B007FC770 /* TabSelection.swift in Sources */,
|
37AAF2962674086B007FC770 /* TabSelection.swift in Sources */,
|
||||||
|
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */,
|
37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */,
|
||||||
37AAF28E2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
37AAF28E2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
|
||||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||||
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
37D4B1812671653A00C925CA /* ContentView.swift in Sources */,
|
37D4B1812671653A00C925CA /* ContentView.swift in Sources */,
|
||||||
37AAF2842673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
37AAF2842673791F007FC770 /* SearchedVideosProvider.swift in Sources */,
|
||||||
|
37CEE4BB2677B63F005A1EFE /* StreamResolution.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@StateObject var state = AppState()
|
@StateObject private var state = AppState()
|
||||||
|
|
||||||
@State var tabSelection: TabSelection = .subscriptions
|
@State private var tabSelection: TabSelection = .subscriptions
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
Loading…
Reference in New Issue
Block a user