846 lines
29 KiB
Swift
846 lines
29 KiB
Swift
//
|
|
/*
|
|
* 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 2021 Aiden Vigue & Jellyfin Contributors
|
|
*/
|
|
|
|
import AVKit
|
|
import AVFoundation
|
|
import Combine
|
|
import Defaults
|
|
import JellyfinAPI
|
|
import MediaPlayer
|
|
import TVVLCKit
|
|
import SwiftUI
|
|
import UIKit
|
|
|
|
// TODO: Look at making the VLC player layer a view
|
|
|
|
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 viewModelListeners = Set<AnyCancellable>()
|
|
private var overlayDismissTimer: Timer?
|
|
private var confirmCloseOverlayDismissTimer: Timer?
|
|
|
|
private var currentPlayerTicks: Int64 {
|
|
return Int64(vlcMediaPlayer.time.intValue) * 100_000
|
|
}
|
|
|
|
private var displayingOverlay: Bool {
|
|
return currentOverlayHostingController?.view.alpha ?? 0 > 0
|
|
}
|
|
|
|
private var displayingContentOverlay: Bool {
|
|
return currentOverlayContentHostingController?.view.alpha ?? 0 > 0
|
|
}
|
|
|
|
private var displayingConfirmClose: Bool {
|
|
return currentConfirmCloseHostingController?.view.alpha ?? 0 > 0
|
|
}
|
|
|
|
private lazy var videoContentView = makeVideoContentView()
|
|
private lazy var jumpBackwardOverlayView = makeJumpBackwardOverlayView()
|
|
private lazy var jumpForwardOverlayView = makeJumpForwardOverlayView()
|
|
private var currentOverlayHostingController: UIHostingController<tvOSVLCOverlay>?
|
|
private var currentOverlayContentHostingController: UIHostingController<SmallMediaStreamSelectionView>?
|
|
private var currentConfirmCloseHostingController: UIHostingController<ConfirmCloseOverlay>?
|
|
|
|
// MARK: init
|
|
|
|
init(viewModel: VideoPlayerViewModel) {
|
|
|
|
self.viewModel = viewModel
|
|
self.vlcMediaPlayer = VLCMediaPlayer()
|
|
|
|
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(jumpForwardOverlayView)
|
|
view.addSubview(jumpBackwardOverlayView)
|
|
|
|
jumpBackwardOverlayView.alpha = 0
|
|
jumpForwardOverlayView.alpha = 0
|
|
}
|
|
|
|
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([
|
|
jumpBackwardOverlayView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 300),
|
|
jumpBackwardOverlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
|
])
|
|
NSLayoutConstraint.activate([
|
|
jumpForwardOverlayView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -300),
|
|
jumpForwardOverlayView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
|
])
|
|
}
|
|
|
|
// 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
|
|
|
|
setupMediaPlayer(newViewModel: viewModel)
|
|
|
|
setupPanGestureRecognizer()
|
|
|
|
addButtonPressRecognizer(pressType: .menu, action: #selector(didPressMenu))
|
|
|
|
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 makeJumpBackwardOverlayView() -> UIImageView {
|
|
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 72)
|
|
let forwardSymbolImage = UIImage(systemName: viewModel.jumpBackwardLength.backwardImageLabel, withConfiguration: symbolConfig)
|
|
let imageView = UIImageView(image: forwardSymbolImage)
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
return imageView
|
|
}
|
|
|
|
private func makeJumpForwardOverlayView() -> UIImageView {
|
|
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 72)
|
|
let forwardSymbolImage = UIImage(systemName: viewModel.jumpForwardLength.forwardImageLabel, withConfiguration: symbolConfig)
|
|
let imageView = UIImageView(image: forwardSymbolImage)
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
return imageView
|
|
}
|
|
|
|
private func setupPanGestureRecognizer() {
|
|
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(userPanned(panGestureRecognizer:)))
|
|
view.addGestureRecognizer(panGestureRecognizer)
|
|
}
|
|
|
|
// MARK: pressesBegan
|
|
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
|
guard let buttonPress = presses.first?.type else { return }
|
|
|
|
switch(buttonPress) {
|
|
case .menu: () // Captured by other recognizer
|
|
case .playPause:
|
|
hideConfirmCloseOverlay()
|
|
|
|
didSelectMain()
|
|
case .select:
|
|
hideConfirmCloseOverlay()
|
|
|
|
didGenerallyTap()
|
|
case .upArrow:
|
|
hideConfirmCloseOverlay()
|
|
case .downArrow:
|
|
hideConfirmCloseOverlay()
|
|
|
|
if Defaults[.downActionShowsMenu] {
|
|
if !displayingContentOverlay {
|
|
didSelectMenu()
|
|
}
|
|
}
|
|
case .leftArrow:
|
|
hideConfirmCloseOverlay()
|
|
|
|
if !displayingContentOverlay {
|
|
didSelectBackward()
|
|
}
|
|
case .rightArrow:
|
|
hideConfirmCloseOverlay()
|
|
|
|
if !displayingContentOverlay {
|
|
didSelectForward()
|
|
}
|
|
case .pageUp: ()
|
|
case .pageDown: ()
|
|
@unknown default: ()
|
|
}
|
|
}
|
|
|
|
private func addButtonPressRecognizer(pressType: UIPress.PressType, action: Selector) {
|
|
let pressRecognizer = UITapGestureRecognizer()
|
|
pressRecognizer.addTarget(self, action: action)
|
|
pressRecognizer.allowedPressTypes = [NSNumber(value: pressType.rawValue)]
|
|
view.addGestureRecognizer(pressRecognizer)
|
|
}
|
|
|
|
// MARK: didPressMenu
|
|
@objc private func didPressMenu() {
|
|
if displayingOverlay {
|
|
hideOverlay()
|
|
} else if displayingContentOverlay {
|
|
hideOverlayContent()
|
|
|
|
showOverlay()
|
|
restartOverlayDismissTimer()
|
|
} else if viewModel.confirmClose && !displayingConfirmClose {
|
|
|
|
showConfirmCloseOverlay()
|
|
restartConfirmCloseDismissTimer()
|
|
|
|
} else {
|
|
vlcMediaPlayer.pause()
|
|
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
@objc private func userPanned(panGestureRecognizer: UIPanGestureRecognizer) {
|
|
if displayingOverlay {
|
|
restartOverlayDismissTimer()
|
|
}
|
|
}
|
|
|
|
// MARK: setupOverlayHostingController
|
|
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
|
|
|
|
// TODO: Look at injecting viewModel into the environment so it updates the current overlay
|
|
|
|
// Main 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()
|
|
}
|
|
}
|
|
|
|
let newOverlayView = tvOSVLCOverlay(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
|
|
|
|
// Media Stream selection
|
|
if let currentOverlayContentHostingController = currentOverlayContentHostingController {
|
|
currentOverlayContentHostingController.view.isHidden = true
|
|
|
|
currentOverlayContentHostingController.view.removeFromSuperview()
|
|
currentOverlayContentHostingController.removeFromParent()
|
|
}
|
|
|
|
let newSmallMenuOverlayView = SmallMediaStreamSelectionView(viewModel: viewModel)
|
|
|
|
let newOverlayContentHostingController = UIHostingController(rootView: newSmallMenuOverlayView)
|
|
|
|
newOverlayContentHostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
newOverlayContentHostingController.view.backgroundColor = UIColor.clear
|
|
|
|
newOverlayContentHostingController.view.alpha = 0
|
|
|
|
addChild(newOverlayContentHostingController)
|
|
view.addSubview(newOverlayContentHostingController.view)
|
|
newOverlayContentHostingController.didMove(toParent: self)
|
|
|
|
NSLayoutConstraint.activate([
|
|
newOverlayContentHostingController.view.topAnchor.constraint(equalTo: videoContentView.topAnchor),
|
|
newOverlayContentHostingController.view.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor),
|
|
newOverlayContentHostingController.view.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
|
newOverlayContentHostingController.view.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
|
])
|
|
|
|
self.currentOverlayContentHostingController = newOverlayContentHostingController
|
|
|
|
// Confirm close
|
|
if let currentConfirmCloseHostingController = currentConfirmCloseHostingController {
|
|
currentConfirmCloseHostingController.view.isHidden = true
|
|
|
|
currentConfirmCloseHostingController.view.removeFromSuperview()
|
|
currentConfirmCloseHostingController.removeFromParent()
|
|
}
|
|
|
|
let newConfirmCloseOverlay = ConfirmCloseOverlay()
|
|
|
|
let newConfirmCloseHostingController = UIHostingController(rootView: newConfirmCloseOverlay)
|
|
|
|
newConfirmCloseHostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
newConfirmCloseHostingController.view.backgroundColor = UIColor.clear
|
|
|
|
newConfirmCloseHostingController.view.alpha = 0
|
|
|
|
addChild(newConfirmCloseHostingController)
|
|
view.addSubview(newConfirmCloseHostingController.view)
|
|
newConfirmCloseHostingController.didMove(toParent: self)
|
|
|
|
NSLayoutConstraint.activate([
|
|
newConfirmCloseHostingController.view.topAnchor.constraint(equalTo: videoContentView.topAnchor),
|
|
newConfirmCloseHostingController.view.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor),
|
|
newConfirmCloseHostingController.view.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
|
newConfirmCloseHostingController.view.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
|
])
|
|
|
|
self.currentConfirmCloseHostingController = newConfirmCloseHostingController
|
|
|
|
// 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) {
|
|
|
|
// remove old player
|
|
|
|
if vlcMediaPlayer.media != nil {
|
|
viewModelListeners.forEach({ $0.cancel() })
|
|
|
|
vlcMediaPlayer.stop()
|
|
viewModel.sendStopReport()
|
|
viewModel.playerOverlayDelegate = nil
|
|
}
|
|
|
|
vlcMediaPlayer = VLCMediaPlayer()
|
|
|
|
// setup with new player and view model
|
|
|
|
vlcMediaPlayer = VLCMediaPlayer()
|
|
|
|
vlcMediaPlayer.delegate = self
|
|
vlcMediaPlayer.drawable = videoContentView
|
|
|
|
// TODO: Custom subtitle sizes
|
|
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
|
|
|
|
stopOverlayDismissTimer()
|
|
|
|
// Stop current media if there is one
|
|
if vlcMediaPlayer.media != nil {
|
|
viewModelListeners.forEach({ $0.cancel() })
|
|
|
|
vlcMediaPlayer.stop()
|
|
viewModel.sendStopReport()
|
|
viewModel.playerOverlayDelegate = nil
|
|
}
|
|
|
|
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
|
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
|
|
|
// TODO: Custom buffer/cache amounts
|
|
|
|
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 = newViewModel.item.userData?.playedPercentage ?? 0
|
|
|
|
if startPercentage > 0 {
|
|
if viewModel.resumeOffset {
|
|
let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000)
|
|
var startSeconds = round((startPercentage / 100) * videoDurationSeconds)
|
|
startSeconds = startSeconds.subtract(5, floor: 0)
|
|
let newStartPercentage = startSeconds / videoDurationSeconds
|
|
newViewModel.sliderPercentage = newStartPercentage
|
|
} else {
|
|
newViewModel.sliderPercentage = startPercentage / 100
|
|
}
|
|
}
|
|
|
|
viewModel = newViewModel
|
|
}
|
|
|
|
// MARK: startPlayback
|
|
func startPlayback() {
|
|
vlcMediaPlayer.play()
|
|
|
|
// Setup external subtitles
|
|
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
|
|
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
|
|
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
|
|
}
|
|
}
|
|
|
|
setMediaPlayerTimeAtCurrentSlider()
|
|
|
|
viewModel.sendPlayReport()
|
|
|
|
restartOverlayDismissTimer(interval: 5)
|
|
}
|
|
|
|
// MARK: setupViewModelListeners
|
|
|
|
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
|
viewModel.$playbackSpeed.sink { newSpeed in
|
|
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
|
}.store(in: &viewModelListeners)
|
|
|
|
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
|
if sliderIsScrubbing {
|
|
self.didBeginScrubbing()
|
|
} else {
|
|
self.didEndScrubbing()
|
|
}
|
|
}.store(in: &viewModelListeners)
|
|
|
|
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
|
self.didSelectAudioStream(index: newAudioStreamIndex)
|
|
}.store(in: &viewModelListeners)
|
|
|
|
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
|
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
|
}.store(in: &viewModelListeners)
|
|
|
|
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
|
|
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
|
|
}.store(in: &viewModelListeners)
|
|
}
|
|
|
|
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() {
|
|
if displayingOverlay {
|
|
hideOverlay()
|
|
} else {
|
|
showOverlay()
|
|
}
|
|
}
|
|
|
|
private func showOverlayContent() {
|
|
guard let currentOverlayContentHostingController = currentOverlayContentHostingController else { return }
|
|
|
|
guard currentOverlayContentHostingController.view.alpha != 1 else { return }
|
|
|
|
currentOverlayContentHostingController.view.setNeedsFocusUpdate()
|
|
currentOverlayContentHostingController.setNeedsFocusUpdate()
|
|
setNeedsFocusUpdate()
|
|
|
|
UIView.animate(withDuration: 0.2) {
|
|
currentOverlayContentHostingController.view.alpha = 1
|
|
}
|
|
}
|
|
|
|
private func hideOverlayContent() {
|
|
guard let currentOverlayContentHostingController = currentOverlayContentHostingController else { return }
|
|
|
|
guard currentOverlayContentHostingController.view.alpha != 0 else { return }
|
|
|
|
setNeedsFocusUpdate()
|
|
|
|
UIView.animate(withDuration: 0.2) {
|
|
currentOverlayContentHostingController.view.alpha = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Show/Hide Jump
|
|
extension VLCPlayerViewController {
|
|
|
|
private func flashJumpBackwardOverlay() {
|
|
jumpBackwardOverlayView.layer.removeAllAnimations()
|
|
|
|
UIView.animate(withDuration: 0.1) {
|
|
self.jumpBackwardOverlayView.alpha = 1
|
|
} completion: { _ in
|
|
self.hideJumpBackwardOverlay()
|
|
}
|
|
}
|
|
|
|
private func hideJumpBackwardOverlay() {
|
|
UIView.animate(withDuration: 0.3) {
|
|
self.jumpBackwardOverlayView.alpha = 0
|
|
}
|
|
}
|
|
|
|
private func flashJumpFowardOverlay() {
|
|
jumpForwardOverlayView.layer.removeAllAnimations()
|
|
|
|
UIView.animate(withDuration: 0.1) {
|
|
self.jumpForwardOverlayView.alpha = 1
|
|
} completion: { _ in
|
|
self.hideJumpForwardOverlay()
|
|
}
|
|
}
|
|
|
|
private func hideJumpForwardOverlay() {
|
|
UIView.animate(withDuration: 0.3) {
|
|
self.jumpForwardOverlayView.alpha = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Show/Hide Confirm close
|
|
extension VLCPlayerViewController {
|
|
|
|
private func showConfirmCloseOverlay() {
|
|
guard let currentConfirmCloseHostingController = currentConfirmCloseHostingController else { return }
|
|
|
|
UIView.animate(withDuration: 0.2) {
|
|
currentConfirmCloseHostingController.view.alpha = 1
|
|
}
|
|
}
|
|
|
|
private func hideConfirmCloseOverlay() {
|
|
guard let currentConfirmCloseHostingController = currentConfirmCloseHostingController else { return }
|
|
|
|
UIView.animate(withDuration: 0.5) {
|
|
currentConfirmCloseHostingController.view.alpha = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: OverlayTimer
|
|
extension VLCPlayerViewController {
|
|
|
|
private func restartOverlayDismissTimer(interval: Double = 5) {
|
|
self.overlayDismissTimer?.invalidate()
|
|
self.overlayDismissTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(dismissTimerFired), userInfo: nil, repeats: false)
|
|
}
|
|
|
|
@objc private func dismissTimerFired() {
|
|
hideOverlay()
|
|
}
|
|
|
|
private func stopOverlayDismissTimer() {
|
|
overlayDismissTimer?.invalidate()
|
|
}
|
|
}
|
|
|
|
// MARK: Confirm Close Overlay Timer
|
|
extension VLCPlayerViewController {
|
|
|
|
private func restartConfirmCloseDismissTimer() {
|
|
self.confirmCloseOverlayDismissTimer?.invalidate()
|
|
self.confirmCloseOverlayDismissTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(confirmCloseTimerFired), userInfo: nil, repeats: false)
|
|
}
|
|
|
|
@objc private func confirmCloseTimerFired() {
|
|
hideConfirmCloseOverlay()
|
|
}
|
|
|
|
private func stopConfirmCloseDismissTimer() {
|
|
confirmCloseOverlayDismissTimer?.invalidate()
|
|
}
|
|
}
|
|
|
|
// MARK: VLCMediaPlayerDelegate
|
|
extension VLCPlayerViewController: VLCMediaPlayerDelegate {
|
|
|
|
|
|
// MARK: mediaPlayerStateChanged
|
|
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
|
|
|
// Don't show buffering if paused, usually here while scrubbing
|
|
if vlcMediaPlayer.state == .buffering && viewModel.playerState == .paused {
|
|
return
|
|
}
|
|
|
|
viewModel.playerState = vlcMediaPlayer.state
|
|
|
|
if vlcMediaPlayer.state == VLCMediaPlayerState.ended {
|
|
if viewModel.autoplayEnabled && viewModel.nextItemVideoPlayerViewModel != nil {
|
|
didSelectPlayNextItem()
|
|
} else {
|
|
didSelectClose()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: mediaPlayerTimeChanged
|
|
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
|
|
|
if !viewModel.sliderIsScrubbing {
|
|
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)
|
|
}
|
|
|
|
if vlcMediaPlayer.currentAudioTrackIndex != viewModel.selectedAudioStreamIndex {
|
|
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
|
|
}
|
|
|
|
/// Do not call when setting to index -1
|
|
func didSelectSubtitleStream(index: Int) {
|
|
|
|
viewModel.subtitlesEnabled = true
|
|
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index)
|
|
|
|
viewModel.sendProgressReport()
|
|
|
|
lastProgressReportTicks = currentPlayerTicks
|
|
}
|
|
|
|
func didSelectClose() {
|
|
vlcMediaPlayer.stop()
|
|
|
|
viewModel.sendStopReport()
|
|
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
func didToggleSubtitles(newValue: Bool) {
|
|
if newValue {
|
|
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex)
|
|
} else {
|
|
vlcMediaPlayer.currentVideoSubTitleIndex = -1
|
|
}
|
|
}
|
|
|
|
func didSelectMenu() {
|
|
stopOverlayDismissTimer()
|
|
|
|
hideOverlay()
|
|
showOverlayContent()
|
|
}
|
|
|
|
func didSelectBackward() {
|
|
|
|
flashJumpBackwardOverlay()
|
|
|
|
vlcMediaPlayer.jumpBackward(viewModel.jumpBackwardLength.rawValue)
|
|
|
|
if displayingOverlay {
|
|
restartOverlayDismissTimer()
|
|
}
|
|
|
|
viewModel.sendProgressReport()
|
|
|
|
lastProgressReportTicks = currentPlayerTicks
|
|
}
|
|
|
|
func didSelectForward() {
|
|
|
|
flashJumpFowardOverlay()
|
|
|
|
vlcMediaPlayer.jumpForward(viewModel.jumpForwardLength.rawValue)
|
|
|
|
if displayingOverlay {
|
|
restartOverlayDismissTimer()
|
|
}
|
|
|
|
viewModel.sendProgressReport()
|
|
|
|
lastProgressReportTicks = currentPlayerTicks
|
|
}
|
|
|
|
func didSelectMain() {
|
|
|
|
switch viewModel.playerState {
|
|
case .buffering:
|
|
vlcMediaPlayer.play()
|
|
restartOverlayDismissTimer()
|
|
case .playing:
|
|
viewModel.sendPauseReport(paused: true)
|
|
vlcMediaPlayer.pause()
|
|
|
|
showOverlay()
|
|
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()
|
|
|
|
lastProgressReportTicks = currentPlayerTicks
|
|
}
|
|
|
|
func didSelectPlayPreviousItem() {
|
|
if let previousItemVideoPlayerViewModel = viewModel.previousItemVideoPlayerViewModel {
|
|
setupMediaPlayer(newViewModel: previousItemVideoPlayerViewModel)
|
|
startPlayback()
|
|
}
|
|
}
|
|
|
|
func didSelectPlayNextItem() {
|
|
if let nextItemVideoPlayerViewModel = viewModel.nextItemVideoPlayerViewModel {
|
|
setupMediaPlayer(newViewModel: nextItemVideoPlayerViewModel)
|
|
startPlayback()
|
|
}
|
|
}
|
|
}
|