initial port of VLC player to tvOS
This commit is contained in:
parent
5eeea800fc
commit
4c7490b5fa
|
@ -145,15 +145,18 @@ class NativePlayerViewController: AVPlayerViewController {
|
||||||
private func play() {
|
private func play() {
|
||||||
player?.play()
|
player?.play()
|
||||||
|
|
||||||
viewModel.sendPlayReport(startTimeTicks: viewModel.item.userData?.playbackPositionTicks ?? 0)
|
// viewModel.sendPlayReport(startTimeTicks: viewModel.item.userData?.playbackPositionTicks ?? 0)
|
||||||
|
viewModel.sendPlayReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendProgressReport(seconds: Double) {
|
private func sendProgressReport(seconds: Double) {
|
||||||
viewModel.sendProgressReport(ticks: Int64(seconds) * 10_000_000)
|
// viewModel.sendProgressReport(ticks: Int64(seconds) * 10_000_000)
|
||||||
|
viewModel.sendProgressReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stop() {
|
private func stop() {
|
||||||
self.player?.pause()
|
self.player?.pause()
|
||||||
viewModel.sendStopReport(ticks: 10_000_000)
|
viewModel.sendStopReport()
|
||||||
|
// viewModel.sendStopReport(ticks: 10_000_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,612 @@
|
||||||
|
//
|
||||||
|
// PlayerViewController.swift
|
||||||
|
// JellyfinVideoPlayerDev
|
||||||
|
//
|
||||||
|
// Created by Ethan Pippin on 11/12/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVKit
|
||||||
|
import AVFoundation
|
||||||
|
import Combine
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import MediaPlayer
|
||||||
|
import TVVLCKit
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// TODO: Make the VLC player layer a view
|
||||||
|
// This will allow changing media and putting the view somewhere else
|
||||||
|
// in a compact state, like a small viewer while navigating the app
|
||||||
|
|
||||||
|
// TODO: Look at making overlays handle timer and all gesture events
|
||||||
|
|
||||||
|
class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
|
// MARK: variables
|
||||||
|
|
||||||
|
private var viewModel: VideoPlayerViewModel
|
||||||
|
private var vlcMediaPlayer = VLCMediaPlayer()
|
||||||
|
private var lastPlayerTicks: Int64 = 0
|
||||||
|
private var lastProgressReportTicks: Int64 = 0
|
||||||
|
private var viewModelReactCancellables = Set<AnyCancellable>()
|
||||||
|
private var overlayDismissTimer: Timer?
|
||||||
|
|
||||||
|
private var currentPlayerTicks: Int64 {
|
||||||
|
return Int64(vlcMediaPlayer.time.intValue) * 100_000
|
||||||
|
}
|
||||||
|
|
||||||
|
private var displayingOverlay: Bool {
|
||||||
|
// return currentOverlayHostingController?.view.alpha ?? 0 > 0
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var jumpForwardLength: VideoPlayerJumpLength {
|
||||||
|
return Defaults[.videoPlayerJumpForward]
|
||||||
|
}
|
||||||
|
|
||||||
|
private var jumpBackwardLength: VideoPlayerJumpLength {
|
||||||
|
return Defaults[.videoPlayerJumpBackward]
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var videoContentView = makeVideoContentView()
|
||||||
|
private lazy var tapGestureView = makeTapGestureView()
|
||||||
|
// private var currentOverlayHostingController: UIHostingController<VLCPlayerCompactOverlayView>?
|
||||||
|
|
||||||
|
// MARK: init
|
||||||
|
|
||||||
|
init(viewModel: VideoPlayerViewModel) {
|
||||||
|
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
viewModel.playerOverlayDelegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupSubviews() {
|
||||||
|
view.addSubview(videoContentView)
|
||||||
|
view.addSubview(tapGestureView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupConstraints() {
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
videoContentView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
videoContentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
videoContentView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||||
|
videoContentView.rightAnchor.constraint(equalTo: view.rightAnchor)
|
||||||
|
])
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tapGestureView.topAnchor.constraint(equalTo: videoContentView.topAnchor),
|
||||||
|
tapGestureView.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor),
|
||||||
|
tapGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
||||||
|
tapGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: viewWillDisappear
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
didSelectClose()
|
||||||
|
|
||||||
|
let defaultNotificationCenter = NotificationCenter.default
|
||||||
|
defaultNotificationCenter.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil)
|
||||||
|
defaultNotificationCenter.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
|
||||||
|
defaultNotificationCenter.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: viewDidLoad
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
setupSubviews()
|
||||||
|
setupConstraints()
|
||||||
|
|
||||||
|
view.backgroundColor = .black
|
||||||
|
|
||||||
|
// These are kept outside of 'setupMediaPlayer' such that
|
||||||
|
// they aren't unnecessarily set more than once
|
||||||
|
vlcMediaPlayer.delegate = self
|
||||||
|
vlcMediaPlayer.drawable = videoContentView
|
||||||
|
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||||
|
|
||||||
|
setupMediaPlayer(newViewModel: viewModel)
|
||||||
|
|
||||||
|
setupRightSwipedGestureRecognizer()
|
||||||
|
setupLeftSwipedGestureRecognizer()
|
||||||
|
|
||||||
|
let defaultNotificationCenter = NotificationCenter.default
|
||||||
|
defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
|
||||||
|
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
|
||||||
|
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appWillTerminate() {
|
||||||
|
viewModel.sendStopReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appWillResignActive() {
|
||||||
|
showOverlay()
|
||||||
|
|
||||||
|
stopOverlayDismissTimer()
|
||||||
|
|
||||||
|
vlcMediaPlayer.pause()
|
||||||
|
|
||||||
|
viewModel.sendPauseReport(paused: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: subviews
|
||||||
|
|
||||||
|
private func makeVideoContentView() -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.backgroundColor = .black
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeTapGestureView() -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
||||||
|
let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didRightSwipe))
|
||||||
|
rightSwipeGesture.direction = .right
|
||||||
|
let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didLeftSwipe))
|
||||||
|
leftSwipeGesture.direction = .left
|
||||||
|
|
||||||
|
view.addGestureRecognizer(singleTapGesture)
|
||||||
|
view.addGestureRecognizer(rightSwipeGesture)
|
||||||
|
view.addGestureRecognizer(leftSwipeGesture)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func didTap() {
|
||||||
|
self.didGenerallyTap()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func didRightSwipe() {
|
||||||
|
self.didSelectForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func didLeftSwipe() {
|
||||||
|
self.didSelectBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: pressesBegan
|
||||||
|
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||||
|
guard let buttonPress = presses.first?.type else { return }
|
||||||
|
|
||||||
|
switch(buttonPress) {
|
||||||
|
case .menu:
|
||||||
|
print("Menu")
|
||||||
|
case .playPause:
|
||||||
|
didSelectMain()
|
||||||
|
print("Play/Pause")
|
||||||
|
case .select:
|
||||||
|
print("select")
|
||||||
|
case .upArrow:
|
||||||
|
print("Up arrow")
|
||||||
|
case .downArrow:
|
||||||
|
print("Down arrow")
|
||||||
|
case .leftArrow:
|
||||||
|
print("Left arrow")
|
||||||
|
case .rightArrow:
|
||||||
|
print("right arrow")
|
||||||
|
case .pageUp:
|
||||||
|
print("page up")
|
||||||
|
case .pageDown:
|
||||||
|
print("page down")
|
||||||
|
@unknown default: ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRightSwipedGestureRecognizer() {
|
||||||
|
let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedRight))
|
||||||
|
swipeRecognizer.direction = .right
|
||||||
|
view.addGestureRecognizer(swipeRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func swipedRight() {
|
||||||
|
didSelectForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupLeftSwipedGestureRecognizer() {
|
||||||
|
let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedLeft))
|
||||||
|
swipeRecognizer.direction = .left
|
||||||
|
view.addGestureRecognizer(swipeRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func swipedLeft() {
|
||||||
|
didSelectBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: setupOverlayHostingController
|
||||||
|
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
|
||||||
|
|
||||||
|
// // TODO: Look at injecting viewModel into the environment so it updates the current overlay
|
||||||
|
// if let currentOverlayHostingController = currentOverlayHostingController {
|
||||||
|
// // UX fade-out
|
||||||
|
// UIView.animate(withDuration: 0.5) {
|
||||||
|
// currentOverlayHostingController.view.alpha = 0
|
||||||
|
// } completion: { _ in
|
||||||
|
// currentOverlayHostingController.view.isHidden = true
|
||||||
|
//
|
||||||
|
// currentOverlayHostingController.view.removeFromSuperview()
|
||||||
|
// currentOverlayHostingController.removeFromParent()
|
||||||
|
//// self.currentOverlayHostingController = nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let newOverlayView = VLCPlayerCompactOverlayView(viewModel: viewModel)
|
||||||
|
// let newOverlayHostingController = UIHostingController(rootView: newOverlayView)
|
||||||
|
//
|
||||||
|
// newOverlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// newOverlayHostingController.view.backgroundColor = UIColor.clear
|
||||||
|
//
|
||||||
|
// // UX fade-in
|
||||||
|
// newOverlayHostingController.view.alpha = 0
|
||||||
|
//
|
||||||
|
// addChild(newOverlayHostingController)
|
||||||
|
// view.addSubview(newOverlayHostingController.view)
|
||||||
|
// newOverlayHostingController.didMove(toParent: self)
|
||||||
|
//
|
||||||
|
// NSLayoutConstraint.activate([
|
||||||
|
// newOverlayHostingController.view.topAnchor.constraint(equalTo: videoContentView.topAnchor),
|
||||||
|
// newOverlayHostingController.view.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor),
|
||||||
|
// newOverlayHostingController.view.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
||||||
|
// newOverlayHostingController.view.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
||||||
|
// ])
|
||||||
|
//
|
||||||
|
// // UX fade-in
|
||||||
|
// UIView.animate(withDuration: 0.5) {
|
||||||
|
// newOverlayHostingController.view.alpha = 1
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// self.currentOverlayHostingController = newOverlayHostingController
|
||||||
|
//
|
||||||
|
// // There is a behavior when setting this that the navigation bar
|
||||||
|
// // on the current navigation controller pops up, re-hide it
|
||||||
|
// self.navigationController?.isNavigationBarHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: setupMediaPlayer
|
||||||
|
extension VLCPlayerViewController {
|
||||||
|
|
||||||
|
/// Main function that handles setting up the media player with the current VideoPlayerViewModel
|
||||||
|
/// and also takes the role of setting the 'viewModel' property with the given viewModel
|
||||||
|
///
|
||||||
|
/// Use case for this is setting new media within the same VLCPlayerViewController
|
||||||
|
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
|
||||||
|
|
||||||
|
stopOverlayDismissTimer()
|
||||||
|
|
||||||
|
// Stop current media if there is one
|
||||||
|
if vlcMediaPlayer.media != nil {
|
||||||
|
viewModelReactCancellables.forEach({ $0.cancel() })
|
||||||
|
|
||||||
|
vlcMediaPlayer.stop()
|
||||||
|
viewModel.sendStopReport()
|
||||||
|
viewModel.playerOverlayDelegate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||||
|
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||||
|
|
||||||
|
let media = VLCMedia(url: newViewModel.streamURL)
|
||||||
|
media.addOption("--prefetch-buffer-size=1048576")
|
||||||
|
media.addOption("--network-caching=5000")
|
||||||
|
|
||||||
|
vlcMediaPlayer.media = media
|
||||||
|
|
||||||
|
setupOverlayHostingController(viewModel: newViewModel)
|
||||||
|
setupViewModelListeners(viewModel: newViewModel)
|
||||||
|
|
||||||
|
newViewModel.getAdjacentEpisodes()
|
||||||
|
newViewModel.playerOverlayDelegate = self
|
||||||
|
|
||||||
|
let startPercentage = viewModel.item.userData?.playedPercentage ?? 0
|
||||||
|
|
||||||
|
if startPercentage > 0 {
|
||||||
|
newViewModel.sliderPercentage = startPercentage / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel = newViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: startPlayback
|
||||||
|
func startPlayback() {
|
||||||
|
vlcMediaPlayer.play()
|
||||||
|
|
||||||
|
setMediaPlayerTimeAtCurrentSlider()
|
||||||
|
|
||||||
|
viewModel.sendPlayReport()
|
||||||
|
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: setupViewModelListeners
|
||||||
|
|
||||||
|
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
||||||
|
viewModel.$playbackSpeed.sink { newSpeed in
|
||||||
|
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
||||||
|
}.store(in: &viewModelReactCancellables)
|
||||||
|
|
||||||
|
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
||||||
|
if sliderIsScrubbing {
|
||||||
|
self.didBeginScrubbing()
|
||||||
|
} else {
|
||||||
|
self.didEndScrubbing()
|
||||||
|
}
|
||||||
|
}.store(in: &viewModelReactCancellables)
|
||||||
|
|
||||||
|
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
||||||
|
self.didSelectAudioStream(index: newAudioStreamIndex)
|
||||||
|
}.store(in: &viewModelReactCancellables)
|
||||||
|
|
||||||
|
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
||||||
|
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
||||||
|
}.store(in: &viewModelReactCancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMediaPlayerTimeAtCurrentSlider() {
|
||||||
|
// Necessary math as VLCMediaPlayer doesn't work well
|
||||||
|
// by just setting the position
|
||||||
|
let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000)
|
||||||
|
let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
||||||
|
let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration)
|
||||||
|
let newPositionOffset = secondsScrubbedTo - videoPosition
|
||||||
|
|
||||||
|
if newPositionOffset > 0 {
|
||||||
|
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
|
||||||
|
} else {
|
||||||
|
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Show/Hide Overlay
|
||||||
|
extension VLCPlayerViewController {
|
||||||
|
|
||||||
|
private func showOverlay() {
|
||||||
|
// guard let overlayHostingController = currentOverlayHostingController else { return }
|
||||||
|
//
|
||||||
|
// guard overlayHostingController.view.alpha != 1 else { return }
|
||||||
|
//
|
||||||
|
// UIView.animate(withDuration: 0.2) {
|
||||||
|
// overlayHostingController.view.alpha = 1
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideOverlay() {
|
||||||
|
// guard let overlayHostingController = currentOverlayHostingController else { return }
|
||||||
|
//
|
||||||
|
// guard overlayHostingController.view.alpha != 0 else { return }
|
||||||
|
//
|
||||||
|
// UIView.animate(withDuration: 0.2) {
|
||||||
|
// overlayHostingController.view.alpha = 0
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleOverlay() {
|
||||||
|
// guard let overlayHostingController = currentOverlayHostingController else { return }
|
||||||
|
//
|
||||||
|
// if overlayHostingController.view.alpha < 1 {
|
||||||
|
// showOverlay()
|
||||||
|
// } else {
|
||||||
|
// hideOverlay()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: OverlayTimer
|
||||||
|
extension VLCPlayerViewController {
|
||||||
|
|
||||||
|
private func restartOverlayDismissTimer(interval: Double = 3) {
|
||||||
|
self.overlayDismissTimer?.invalidate()
|
||||||
|
self.overlayDismissTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(dismissTimerFired), userInfo: nil, repeats: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func dismissTimerFired() {
|
||||||
|
self.hideOverlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopOverlayDismissTimer() {
|
||||||
|
self.overlayDismissTimer?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: VLCMediaPlayerDelegate
|
||||||
|
extension VLCPlayerViewController: VLCMediaPlayerDelegate {
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: mediaPlayerStateChanged
|
||||||
|
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
||||||
|
|
||||||
|
viewModel.playerState = vlcMediaPlayer.state
|
||||||
|
|
||||||
|
if vlcMediaPlayer.state == VLCMediaPlayerState.ended {
|
||||||
|
if viewModel.autoPlayNextItem && viewModel.shouldShowAutoPlayNextItem && viewModel.nextItemVideoPlayerViewModel != nil {
|
||||||
|
didSelectNextItem()
|
||||||
|
} else {
|
||||||
|
didSelectClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: mediaPlayerTimeChanged
|
||||||
|
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
||||||
|
|
||||||
|
guard !viewModel.sliderIsScrubbing else {
|
||||||
|
lastPlayerTicks = currentPlayerTicks
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.sliderPercentage = Double(vlcMediaPlayer.position)
|
||||||
|
|
||||||
|
// Have to manually set playing because VLCMediaPlayer doesn't
|
||||||
|
// properly set it itself
|
||||||
|
if abs(currentPlayerTicks - lastPlayerTicks) >= 10_000 {
|
||||||
|
viewModel.playerState = VLCMediaPlayerState.playing
|
||||||
|
}
|
||||||
|
|
||||||
|
// If needing to fix subtitle streams during playback
|
||||||
|
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled {
|
||||||
|
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
|
||||||
|
didSelectAudioStream(index: viewModel.selectedAudioStreamIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPlayerTicks = currentPlayerTicks
|
||||||
|
|
||||||
|
// Send progress report every 5 seconds
|
||||||
|
if abs(lastProgressReportTicks - currentPlayerTicks) >= 500_000_000 {
|
||||||
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
|
lastProgressReportTicks = currentPlayerTicks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: PlayerOverlayDelegate
|
||||||
|
extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
|
|
||||||
|
func didSelectAudioStream(index: Int) {
|
||||||
|
vlcMediaPlayer.currentAudioTrackIndex = Int32(index)
|
||||||
|
|
||||||
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
|
lastProgressReportTicks = currentPlayerTicks
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectSubtitleStream(index: Int) {
|
||||||
|
if viewModel.subtitlesEnabled {
|
||||||
|
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index)
|
||||||
|
} else {
|
||||||
|
vlcMediaPlayer.currentVideoSubTitleIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
|
lastProgressReportTicks = currentPlayerTicks
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectClose() {
|
||||||
|
vlcMediaPlayer.stop()
|
||||||
|
|
||||||
|
viewModel.sendStopReport()
|
||||||
|
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectGoogleCast() {
|
||||||
|
print("didSelectCast")
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectAirplay() {
|
||||||
|
print("didSelectAirplay")
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectCaptions() {
|
||||||
|
|
||||||
|
viewModel.subtitlesEnabled = !viewModel.subtitlesEnabled
|
||||||
|
|
||||||
|
if viewModel.subtitlesEnabled {
|
||||||
|
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex)
|
||||||
|
} else {
|
||||||
|
vlcMediaPlayer.currentVideoSubTitleIndex = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement properly in overlays
|
||||||
|
func didSelectMenu() {
|
||||||
|
stopOverlayDismissTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement properly in overlays
|
||||||
|
func didDeselectMenu() {
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectBackward() {
|
||||||
|
vlcMediaPlayer.jumpBackward(jumpBackwardLength.rawValue)
|
||||||
|
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
|
||||||
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
|
self.lastProgressReportTicks = currentPlayerTicks
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectForward() {
|
||||||
|
vlcMediaPlayer.jumpForward(jumpForwardLength.rawValue)
|
||||||
|
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
|
||||||
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
|
self.lastProgressReportTicks = currentPlayerTicks
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectMain() {
|
||||||
|
|
||||||
|
switch viewModel.playerState {
|
||||||
|
case .buffering:
|
||||||
|
vlcMediaPlayer.play()
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
case .playing:
|
||||||
|
viewModel.sendPauseReport(paused: true)
|
||||||
|
vlcMediaPlayer.pause()
|
||||||
|
restartOverlayDismissTimer(interval: 5)
|
||||||
|
case .paused:
|
||||||
|
viewModel.sendPauseReport(paused: false)
|
||||||
|
vlcMediaPlayer.play()
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
default: ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func didGenerallyTap() {
|
||||||
|
toggleOverlay()
|
||||||
|
|
||||||
|
restartOverlayDismissTimer(interval: 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didBeginScrubbing() {
|
||||||
|
stopOverlayDismissTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func didEndScrubbing() {
|
||||||
|
setMediaPlayerTimeAtCurrentSlider()
|
||||||
|
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
|
||||||
|
viewModel.sendProgressReport()
|
||||||
|
|
||||||
|
self.lastProgressReportTicks = currentPlayerTicks
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectPreviousItem() {
|
||||||
|
setupMediaPlayer(newViewModel: viewModel.previousItemVideoPlayerViewModel!)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectNextItem() {
|
||||||
|
setupMediaPlayer(newViewModel: viewModel.nextItemVideoPlayerViewModel!)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,3 +23,19 @@ struct NativePlayerView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct VLCPlayerView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
|
let viewModel: VideoPlayerViewModel
|
||||||
|
|
||||||
|
typealias UIViewControllerType = VLCPlayerViewController
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> VLCPlayerViewController {
|
||||||
|
|
||||||
|
return VLCPlayerViewController(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: VLCPlayerViewController, context: Context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -253,6 +253,8 @@
|
||||||
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
||||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
||||||
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
|
||||||
|
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; };
|
||||||
|
E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; };
|
||||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; };
|
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; };
|
||||||
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; };
|
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; };
|
||||||
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; };
|
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; };
|
||||||
|
@ -288,6 +290,7 @@
|
||||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
||||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
||||||
|
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E178857C278037FD0094FBCF /* JellyfinAPI */; };
|
||||||
E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
||||||
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
|
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
|
||||||
|
@ -570,6 +573,7 @@
|
||||||
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
|
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
|
||||||
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
|
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
|
||||||
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
|
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
|
||||||
|
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
|
||||||
E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = "<group>"; };
|
E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = "<group>"; };
|
||||||
E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = "<group>"; };
|
E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = "<group>"; };
|
||||||
|
@ -650,6 +654,7 @@
|
||||||
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
||||||
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */,
|
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */,
|
||||||
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
||||||
|
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */,
|
||||||
E12186DE2718F1C50010884C /* Defaults in Frameworks */,
|
E12186DE2718F1C50010884C /* Defaults in Frameworks */,
|
||||||
53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */,
|
53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */,
|
||||||
363CADF08820D3B2055CF1D8 /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
363CADF08820D3B2055CF1D8 /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
||||||
|
@ -710,6 +715,7 @@
|
||||||
E1C812C7277AE40900918266 /* NativePlayerViewController.swift */,
|
E1C812C7277AE40900918266 /* NativePlayerViewController.swift */,
|
||||||
E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */,
|
E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */,
|
||||||
E1C812C8277AE40900918266 /* VideoPlayerView.swift */,
|
E1C812C8277AE40900918266 /* VideoPlayerView.swift */,
|
||||||
|
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */,
|
||||||
);
|
);
|
||||||
path = VideoPlayer;
|
path = VideoPlayer;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1419,6 +1425,7 @@
|
||||||
E1218C9D271A2CD600EA0737 /* CombineExt */,
|
E1218C9D271A2CD600EA0737 /* CombineExt */,
|
||||||
E1218C9F271A2CF200EA0737 /* Nuke */,
|
E1218C9F271A2CF200EA0737 /* Nuke */,
|
||||||
E1A9999A271A343C008E78C0 /* SwiftUICollection */,
|
E1A9999A271A343C008E78C0 /* SwiftUICollection */,
|
||||||
|
E178857C278037FD0094FBCF /* JellyfinAPI */,
|
||||||
);
|
);
|
||||||
productName = "JellyfinPlayer tvOS";
|
productName = "JellyfinPlayer tvOS";
|
||||||
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
|
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
|
||||||
|
@ -1826,6 +1833,7 @@
|
||||||
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
||||||
53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */,
|
53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */,
|
||||||
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
|
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
|
||||||
|
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */,
|
||||||
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||||
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||||
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||||
|
@ -1883,6 +1891,7 @@
|
||||||
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
|
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
|
||||||
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
|
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
|
||||||
E19169CF272514760085832A /* HTTPScheme.swift in Sources */,
|
E19169CF272514760085832A /* HTTPScheme.swift in Sources */,
|
||||||
|
E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */,
|
||||||
E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */,
|
E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
||||||
E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */,
|
E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||||
|
@ -2810,6 +2819,11 @@
|
||||||
package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */;
|
package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||||
productName = Defaults;
|
productName = Defaults;
|
||||||
};
|
};
|
||||||
|
E178857C278037FD0094FBCF /* JellyfinAPI */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */;
|
||||||
|
productName = JellyfinAPI;
|
||||||
|
};
|
||||||
E1A99998271A3429008E78C0 /* SwiftUICollection */ = {
|
E1A99998271A3429008E78C0 /* SwiftUICollection */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
||||||
|
|
|
@ -19,6 +19,8 @@ import UIKit
|
||||||
// This will allow changing media and putting the view somewhere else
|
// This will allow changing media and putting the view somewhere else
|
||||||
// in a compact state, like a small viewer while navigating the app
|
// in a compact state, like a small viewer while navigating the app
|
||||||
|
|
||||||
|
// TODO: Look at making overlays handle timer and all gesture events
|
||||||
|
|
||||||
class VLCPlayerViewController: UIViewController {
|
class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
// MARK: variables
|
// MARK: variables
|
||||||
|
|
|
@ -27,7 +27,10 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
NativePlayerView(viewModel: viewModel)
|
// NativePlayerView(viewModel: viewModel)
|
||||||
|
// .navigationBarHidden(true)
|
||||||
|
// .ignoresSafeArea()
|
||||||
|
VLCPlayerView(viewModel: viewModel)
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue