ios live tv and experimental settings

This commit is contained in:
jhays 2022-04-27 22:46:14 -05:00
parent 5531c912ea
commit d649dd88cf
11 changed files with 401 additions and 113 deletions

View File

@ -27,14 +27,14 @@ final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable {
@ViewBuilder
func makeStart() -> some View {
// if Defaults[.Experimental.liveTVNativePlayer] {
// LiveTVNativeVideoPlayerView(viewModel: viewModel)
// .navigationBarHidden(true)
// .ignoresSafeArea()
// } else {
if Defaults[.Experimental.liveTVNativePlayer] {
LiveTVNativePlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
} else {
LiveTVPlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
// }
}
}
}

View File

@ -263,6 +263,7 @@
C4534981279A3F140045F1E2 /* tvOSLiveTVOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */; };
C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */; };
C4534985279A40C60045F1E2 /* LiveTVVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534984279A40C50045F1E2 /* LiveTVVideoPlayerView.swift */; };
C45640D0281A43EF007096DE /* LiveTVNativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45640CF281A43EF007096DE /* LiveTVNativePlayerViewController.swift */; };
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */; };
C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942C827F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift */; };
@ -751,6 +752,7 @@
C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVOverlay.swift; sourceTree = "<group>"; };
C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
C4534984279A40C50045F1E2 /* LiveTVVideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVVideoPlayerView.swift; sourceTree = "<group>"; };
C45640CF281A43EF007096DE /* LiveTVNativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVNativePlayerViewController.swift; sourceTree = "<group>"; };
C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVCoordinator.swift; sourceTree = "<group>"; };
C45942C827F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLiveTVVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
C45942CA27F6984100C54FE7 /* LiveTVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVPlayerViewController.swift; sourceTree = "<group>"; };
@ -1742,6 +1744,7 @@
isa = PBXGroup;
children = (
E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */,
C45640CF281A43EF007096DE /* LiveTVNativePlayerViewController.swift */,
E1002B692793E12E00E47059 /* Overlays */,
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */,
@ -2515,6 +2518,7 @@
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */,
E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
C45640D0281A43EF007096DE /* LiveTVNativePlayerViewController.swift in Sources */,
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,

View File

@ -22,10 +22,10 @@
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.100",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
@ -40,10 +40,10 @@
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.100",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,56 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.100",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.100",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,56 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.250",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.250",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,56 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -62,97 +62,91 @@ struct LiveTVChannelItemWideElement: View {
var body: some View {
ZStack {
HStack {
ZStack(alignment: .center) {
ImageView(channel.getPrimaryImage(maxWidth: 128))
.aspectRatio(contentMode: .fit)
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
VStack(alignment: .center) {
Spacer()
.frame(maxHeight: .infinity)
GeometryReader { gp in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3)
.fill(Color.gray)
.opacity(0.4)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 6, maxHeight: 6)
RoundedRectangle(cornerRadius: 6)
.fill(Color.jellyfinPurple)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 6)
ZStack {
HStack {
ZStack(alignment: .center) {
ImageView(channel.getPrimaryImage(maxWidth: 128))
.aspectRatio(contentMode: .fit)
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
VStack(alignment: .center) {
Spacer()
.frame(maxHeight: .infinity)
GeometryReader { gp in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3)
.fill(Color.gray)
.opacity(0.4)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 6, maxHeight: 6)
RoundedRectangle(cornerRadius: 6)
.fill(Color.jellyfinPurple)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 6)
}
}
.frame(height: 6, alignment: .center)
.padding(.init(top: 0, leading: 4, bottom: 0, trailing: 4))
}
.frame(height: 6, alignment: .center)
.padding(.init(top: 0, leading: 4, bottom: 0, trailing: 4))
}
if loading {
ProgressView()
}
}
.aspectRatio(1.0, contentMode: .fit)
VStack(alignment: .leading) {
let channelNumber = channel.number != nil ? "\(channel.number ?? "") " : ""
let channelName = "\(channelNumber)\(channel.name ?? "?")"
Text(channelName)
.font(.body)
.lineLimit(1)
.frame(alignment: .leading)
HStack(alignment: .top) {
Text(currentProgramText.timeDisplay)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.green)
.frame(width: 40)
Text(currentProgramText.title)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.green)
}
if nextProgramsText.count > 0,
let nextItem = nextProgramsText[0] {
HStack(alignment: .top) {
Text(nextItem.timeDisplay)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
.frame(width: 40)
Text(nextItem.title)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
if loading {
ProgressView()
}
}
if nextProgramsText.count > 1,
let nextItem2 = nextProgramsText[1] {
HStack(alignment: .top) {
Text(nextItem2.timeDisplay)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
.frame(width: 40)
Text(nextItem2.title)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
.aspectRatio(1.0, contentMode: .fit)
VStack(alignment: .leading) {
let channelNumber = channel.number != nil ? "\(channel.number ?? "") " : ""
let channelName = "\(channelNumber)\(channel.name ?? "?")"
Text(channelName)
.font(.body)
.lineLimit(1)
.foregroundColor(Color.jellyfinPurple)
.frame(alignment: .leading)
.padding(.init(top: 0, leading: 0, bottom: 4, trailing: 0))
programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title, color: Color("TextHighlightColor"))
if nextProgramsText.count > 0,
let nextItem = nextProgramsText[0] {
programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray)
}
if nextProgramsText.count > 1,
let nextItem2 = nextProgramsText[1] {
programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray)
}
Spacer()
}
Spacer()
}
Spacer()
.frame(alignment: .leading)
.padding()
.opacity(loading ? 0.5 : 1.0)
}
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color("BackgroundSecondaryColor"))
)
.frame(height: 128)
.onTapGesture {
onSelect { loadingState in
loading = loadingState
}
}
.frame(alignment: .leading)
.padding()
.opacity(loading ? 0.5 : 1.0)
}
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous).fill(Color("BackgroundColor"))
)
.frame(height: 128)
.onTapGesture {
onSelect { loadingState in
loading = loadingState
}
.background{
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color("BackgroundColor"))
.shadow(color: Color("ShadowColor"), radius: 4, x: 0, y: 0)
}
}
@ViewBuilder
func programLabel(timeText: String, titleText: String, color: Color) -> some View {
HStack(alignment: .top) {
Text(timeText)
.font(.footnote)
.lineLimit(2)
.foregroundColor(color)
.frame(width: 38, alignment: .leading)
Text(titleText)
.font(.footnote)
.lineLimit(2)
.foregroundColor(color)
}
}
}

View File

@ -103,7 +103,7 @@ struct LiveTVChannelsView: View {
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(132)
heightDimension: .absolute(144)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
@ -114,7 +114,7 @@ struct LiveTVChannelsView: View {
} else {
if isPortrait {
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute(UIScreen.main.bounds.width - 2),
widthDimension: .absolute(UIScreen.main.bounds.width - 32),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
@ -124,7 +124,7 @@ struct LiveTVChannelsView: View {
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(132)
heightDimension: .absolute(144)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
@ -149,7 +149,7 @@ struct LiveTVChannelsView: View {
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(132)
heightDimension: .absolute(144)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,

View File

@ -19,7 +19,11 @@ struct ExperimentalSettingsView: View {
var nativePlayer
@Default(.Experimental.liveTVAlphaEnabled)
var liveTVAlphaEnabled
@Default(.Experimental.liveTVForceDirectPlay)
var liveTVForceDirectPlay
@Default(.Experimental.liveTVNativePlayer)
var liveTVNativePlayer
var body: some View {
Form {
Section {
@ -38,6 +42,10 @@ struct ExperimentalSettingsView: View {
Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled)
Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay)
Toggle("Live TV Native Player", isOn: $liveTVNativePlayer)
} header: {
Text("Live TV")
}

View File

@ -0,0 +1,114 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import AVKit
import Combine
import JellyfinAPI
import UIKit
class LiveTVNativePlayerViewController: AVPlayerViewController {
let viewModel: VideoPlayerViewModel
var timeObserverToken: Any?
var lastProgressTicks: Int64 = 0
private var cancellables = Set<AnyCancellable>()
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
let player: AVPlayer
if let transcodedStreamURL = viewModel.transcodedStreamURL {
player = AVPlayer(url: transcodedStreamURL)
} else {
player = AVPlayer(url: viewModel.hlsStreamURL)
}
player.appliesMediaSelectionCriteriaAutomatically = false
let timeScale = CMTimeScale(NSEC_PER_SEC)
let time = CMTime(seconds: 5, preferredTimescale: timeScale)
timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
if time.seconds != 0 {
self?.sendProgressReport(seconds: time.seconds)
}
}
self.player = player
self.allowsPictureInPicturePlayback = true
self.player?.allowsExternalPlayback = true
}
private func createMetadataItem(for identifier: AVMetadataIdentifier,
value: Any) -> AVMetadataItem
{
let item = AVMutableMetadataItem()
item.identifier = identifier
item.value = value as? NSCopying & NSObjectProtocol
// Specify "und" to indicate an undefined language.
item.extendedLanguageTag = "und"
return item.copy() as! AVMetadataItem
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stop()
removePeriodicTimeObserver()
}
func removePeriodicTimeObserver() {
if let timeObserverToken = timeObserverToken {
player?.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
player?.seek(to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000),
toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, timescale: 1),
completionHandler: { _ in
self.play()
})
}
private func play() {
player?.play()
viewModel.sendPlayReport()
}
private func sendProgressReport(seconds: Double) {
viewModel.setSeconds(Int64(seconds))
viewModel.sendProgressReport()
}
private func stop() {
self.player?.pause()
viewModel.sendStopReport()
}
}

View File

@ -9,19 +9,19 @@
import SwiftUI
import UIKit
//struct NativePlayerView: UIViewControllerRepresentable {
//
// let viewModel: VideoPlayerViewModel
//
// typealias UIViewControllerType = NativePlayerViewController
//
// func makeUIViewController(context: Context) -> NativePlayerViewController {
//
// NativePlayerViewController(viewModel: viewModel)
// }
//
// func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {}
//}
struct LiveTVNativePlayerView: UIViewControllerRepresentable {
let viewModel: VideoPlayerViewModel
typealias UIViewControllerType = LiveTVNativePlayerViewController
func makeUIViewController(context: Context) -> LiveTVNativePlayerViewController {
LiveTVNativePlayerViewController(viewModel: viewModel)
}
func updateUIViewController(_ uiViewController: LiveTVNativePlayerViewController, context: Context) {}
}
struct LiveTVPlayerView: UIViewControllerRepresentable {