initial previous and next item feature
This commit is contained in:
parent
99445e387c
commit
fe0c8ee03b
|
@ -61,6 +61,24 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
|
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
|
|
||||||
|
if viewModel.showAdjacentItems {
|
||||||
|
Button {
|
||||||
|
viewModel.playerOverlayDelegate?.didSelectPreviousItem()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "chevron.left.circle")
|
||||||
|
}
|
||||||
|
.disabled(viewModel.previousItemVideoPlayerViewModel == nil)
|
||||||
|
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
viewModel.playerOverlayDelegate?.didSelectNextItem()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "chevron.right.circle")
|
||||||
|
}
|
||||||
|
.disabled(viewModel.nextItemVideoPlayerViewModel == nil)
|
||||||
|
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
|
||||||
|
}
|
||||||
|
|
||||||
if viewModel.shouldShowGoogleCast {
|
if viewModel.shouldShowGoogleCast {
|
||||||
Button {
|
Button {
|
||||||
viewModel.playerOverlayDelegate?.didSelectGoogleCast()
|
viewModel.playerOverlayDelegate?.didSelectGoogleCast()
|
||||||
|
@ -205,8 +223,8 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
|
||||||
viewModel.playerOverlayDelegate?.didSelectMain()
|
viewModel.playerOverlayDelegate?.didSelectMain()
|
||||||
} label: {
|
} label: {
|
||||||
mainButtonView
|
mainButtonView
|
||||||
.padding(.horizontal, 5)
|
|
||||||
.frame(minWidth: 30, maxWidth: 30)
|
.frame(minWidth: 30, maxWidth: 30)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
@ -291,7 +309,8 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
||||||
subtitlesEnabled: true,
|
subtitlesEnabled: true,
|
||||||
sliderPercentage: 0.432,
|
sliderPercentage: 0.432,
|
||||||
selectedAudioStreamIndex: -1,
|
selectedAudioStreamIndex: -1,
|
||||||
selectedSubtitleStreamIndex: -1))
|
selectedSubtitleStreamIndex: -1,
|
||||||
|
showAdjacentItems: true))
|
||||||
}
|
}
|
||||||
.previewInterfaceOrientation(.landscapeLeft)
|
.previewInterfaceOrientation(.landscapeLeft)
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,20 +239,11 @@ struct VLCPlayerOverlayView_Previews: PreviewProvider {
|
||||||
subtitlesEnabled: true,
|
subtitlesEnabled: true,
|
||||||
sliderPercentage: 0.0,
|
sliderPercentage: 0.0,
|
||||||
selectedAudioStreamIndex: -1,
|
selectedAudioStreamIndex: -1,
|
||||||
selectedSubtitleStreamIndex: -1))
|
selectedSubtitleStreamIndex: -1,
|
||||||
|
showAdjacentItems: true))
|
||||||
}
|
}
|
||||||
.previewInterfaceOrientation(.landscapeLeft)
|
.previewInterfaceOrientation(.landscapeLeft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HorizontalAlignment {
|
|
||||||
|
|
||||||
private struct EpisodeSeriesTitleAlignment: AlignmentID {
|
|
||||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
|
||||||
context[HorizontalAlignment.leading]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static let EpisodeSeriesAlignmentGuide = HorizontalAlignment(EpisodeSeriesTitleAlignment.self)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,3 +12,15 @@ import SwiftUI
|
||||||
protocol VideoPlayerOverlay: View {
|
protocol VideoPlayerOverlay: View {
|
||||||
var viewModel: VideoPlayerViewModel { get set }
|
var viewModel: VideoPlayerViewModel { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension HorizontalAlignment {
|
||||||
|
|
||||||
|
private struct EpisodeSeriesTitleAlignment: AlignmentID {
|
||||||
|
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||||
|
context[HorizontalAlignment.leading]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let EpisodeSeriesAlignmentGuide = HorizontalAlignment(EpisodeSeriesTitleAlignment.self)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -27,4 +27,7 @@ protocol PlayerOverlayDelegate {
|
||||||
|
|
||||||
func didSelectAudioStream(index: Int)
|
func didSelectAudioStream(index: Int)
|
||||||
func didSelectSubtitleStream(index: Int)
|
func didSelectSubtitleStream(index: Int)
|
||||||
|
|
||||||
|
func didSelectPreviousItem()
|
||||||
|
func didSelectNextItem()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,18 @@ import MobileVLCKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
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
|
||||||
|
|
||||||
class VLCPlayerViewController: UIViewController {
|
class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
// MARK: variables
|
// MARK: variables
|
||||||
|
|
||||||
private let viewModel: VideoPlayerViewModel
|
private var viewModel: VideoPlayerViewModel
|
||||||
private var vlcMediaPlayer = VLCMediaPlayer()
|
private var vlcMediaPlayer = VLCMediaPlayer()
|
||||||
private var lastPlayerTicks: Int64
|
private var lastPlayerTicks: Int64 = 0
|
||||||
private var lastProgressReportTicks: Int64
|
private var lastProgressReportTicks: Int64 = 0
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var overlayDismissTimer: Timer?
|
private var overlayDismissTimer: Timer?
|
||||||
|
|
||||||
|
@ -31,7 +35,7 @@ class VLCPlayerViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var displayingOverlay: Bool {
|
private var displayingOverlay: Bool {
|
||||||
return overlayHostingController.view.alpha > 0
|
return currentOverlayHostingController?.view.alpha ?? 0 > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var jumpForwardLength: VideoPlayerJumpLength {
|
private var jumpForwardLength: VideoPlayerJumpLength {
|
||||||
|
@ -44,7 +48,7 @@ class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
private lazy var videoContentView = makeVideoContentView()
|
private lazy var videoContentView = makeVideoContentView()
|
||||||
private lazy var tapGestureView = makeTapGestureView()
|
private lazy var tapGestureView = makeTapGestureView()
|
||||||
private lazy var overlayHostingController = makeOverlayHostingController()
|
private var currentOverlayHostingController: UIHostingController<VLCPlayerCompactOverlayView>?
|
||||||
|
|
||||||
// MARK: init
|
// MARK: init
|
||||||
|
|
||||||
|
@ -52,9 +56,6 @@ class VLCPlayerViewController: UIViewController {
|
||||||
|
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
||||||
self.lastPlayerTicks = viewModel.item.userData?.playbackPositionTicks ?? 0
|
|
||||||
self.lastProgressReportTicks = viewModel.item.userData?.playbackPositionTicks ?? 0
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
viewModel.playerOverlayDelegate = self
|
viewModel.playerOverlayDelegate = self
|
||||||
|
@ -67,12 +68,6 @@ class VLCPlayerViewController: UIViewController {
|
||||||
private func setupSubviews() {
|
private func setupSubviews() {
|
||||||
view.addSubview(videoContentView)
|
view.addSubview(videoContentView)
|
||||||
view.addSubview(tapGestureView)
|
view.addSubview(tapGestureView)
|
||||||
|
|
||||||
addChild(overlayHostingController)
|
|
||||||
overlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
overlayHostingController.view.backgroundColor = UIColor.black.withAlphaComponent(0.2)
|
|
||||||
view.addSubview(overlayHostingController.view)
|
|
||||||
overlayHostingController.didMove(toParent: self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupConstraints() {
|
private func setupConstraints() {
|
||||||
|
@ -88,12 +83,6 @@ class VLCPlayerViewController: UIViewController {
|
||||||
tapGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
tapGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor),
|
||||||
tapGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
tapGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor)
|
||||||
])
|
])
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
overlayHostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
overlayHostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
overlayHostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
|
|
||||||
overlayHostingController.view.rightAnchor.constraint(equalTo: view.rightAnchor)
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: viewWillAppear
|
// MARK: viewWillAppear
|
||||||
|
@ -120,39 +109,15 @@ class VLCPlayerViewController: UIViewController {
|
||||||
setupSubviews()
|
setupSubviews()
|
||||||
setupConstraints()
|
setupConstraints()
|
||||||
|
|
||||||
setupViewModelListeners()
|
|
||||||
|
|
||||||
view.backgroundColor = .black
|
view.backgroundColor = .black
|
||||||
|
|
||||||
setupMediaPlayer()
|
// These are kept outside of 'setupMediaPlayer' such that
|
||||||
}
|
// they aren't unnecessarily set more than once
|
||||||
|
vlcMediaPlayer.delegate = self
|
||||||
// MARK: setupViewModelListeners
|
vlcMediaPlayer.drawable = videoContentView
|
||||||
|
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||||
private func setupViewModelListeners() {
|
|
||||||
viewModel.$playbackSpeed.sink { newSpeed in
|
|
||||||
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
|
||||||
}.store(in: &cancellables)
|
|
||||||
|
|
||||||
viewModel.$screenFilled.sink { shouldFill in
|
setupMediaPlayer(newViewModel: viewModel)
|
||||||
self.changeFill(to: shouldFill)
|
|
||||||
}.store(in: &cancellables)
|
|
||||||
|
|
||||||
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
|
||||||
if sliderIsScrubbing {
|
|
||||||
self.didBeginScrubbing()
|
|
||||||
} else {
|
|
||||||
self.didEndScrubbing(position: self.viewModel.sliderPercentage)
|
|
||||||
}
|
|
||||||
}.store(in: &cancellables)
|
|
||||||
|
|
||||||
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
|
||||||
self.didSelectAudioStream(index: newAudioStreamIndex)
|
|
||||||
}.store(in: &cancellables)
|
|
||||||
|
|
||||||
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
|
||||||
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
|
||||||
}.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func changeFill(to shouldFill: Bool) {
|
private func changeFill(to shouldFill: Bool) {
|
||||||
|
@ -177,7 +142,6 @@ class VLCPlayerViewController: UIViewController {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
startPlayback()
|
startPlayback()
|
||||||
restartOverlayDismissTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: subviews
|
// MARK: subviews
|
||||||
|
@ -193,7 +157,6 @@ class VLCPlayerViewController: UIViewController {
|
||||||
private func makeTapGestureView() -> UIView {
|
private func makeTapGestureView() -> UIView {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.backgroundColor = .clear
|
|
||||||
|
|
||||||
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
||||||
let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didRightSwipe))
|
let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didRightSwipe))
|
||||||
|
@ -220,33 +183,93 @@ class VLCPlayerViewController: UIViewController {
|
||||||
self.didSelectBackward()
|
self.didSelectBackward()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeOverlayHostingController() -> UIHostingController<VLCPlayerCompactOverlayView> {
|
// MARK: setupOverlayHostingController
|
||||||
let overlayView = VLCPlayerCompactOverlayView(viewModel: viewModel)
|
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
|
||||||
return UIHostingController(rootView: overlayView)
|
|
||||||
|
if let currentOverlayHostingController = currentOverlayHostingController {
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
])
|
||||||
|
|
||||||
|
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
|
// MARK: setupMediaPlayer
|
||||||
extension VLCPlayerViewController {
|
extension VLCPlayerViewController {
|
||||||
|
|
||||||
func setupMediaPlayer() {
|
/// 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) {
|
||||||
|
|
||||||
vlcMediaPlayer.delegate = self
|
stopOverlayDismissTimer()
|
||||||
vlcMediaPlayer.drawable = videoContentView
|
|
||||||
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
// UX improvement
|
||||||
|
(vlcMediaPlayer.drawable as! UIView).isHidden = true
|
||||||
|
|
||||||
|
// Stop current media if there is one
|
||||||
|
if vlcMediaPlayer.media != nil {
|
||||||
|
cancellables.forEach({ $0.cancel() })
|
||||||
|
|
||||||
|
vlcMediaPlayer.stop()
|
||||||
|
viewModel.sendStopReport()
|
||||||
|
viewModel.playerOverlayDelegate = nil
|
||||||
|
vlcMediaPlayer.media = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||||
|
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
|
||||||
|
|
||||||
let media = VLCMedia(url: viewModel.streamURL)
|
let media = VLCMedia(url: newViewModel.streamURL)
|
||||||
media.addOption("--prefetch-buffer-size=1048576")
|
media.addOption("--prefetch-buffer-size=1048576")
|
||||||
media.addOption("--network-caching=5000")
|
media.addOption("--network-caching=5000")
|
||||||
|
|
||||||
vlcMediaPlayer.media = media
|
vlcMediaPlayer.media = media
|
||||||
|
|
||||||
|
setupOverlayHostingController(viewModel: newViewModel)
|
||||||
|
setupViewModelListeners(viewModel: newViewModel)
|
||||||
|
|
||||||
|
newViewModel.getAdjacentEpisodes()
|
||||||
|
newViewModel.playerOverlayDelegate = self
|
||||||
|
|
||||||
|
viewModel = newViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func startPlayback() {
|
func startPlayback() {
|
||||||
|
// UX improvement
|
||||||
|
(vlcMediaPlayer.drawable as! UIView).isHidden = false
|
||||||
|
|
||||||
vlcMediaPlayer.play()
|
vlcMediaPlayer.play()
|
||||||
|
|
||||||
viewModel.sendPlayReport()
|
viewModel.sendPlayReport()
|
||||||
|
|
||||||
|
restartOverlayDismissTimer()
|
||||||
|
|
||||||
// 1 second = 10,000,000 ticks
|
// 1 second = 10,000,000 ticks
|
||||||
let startTicks: Int64 = viewModel.item.userData?.playbackPositionTicks ?? 0
|
let startTicks: Int64 = viewModel.item.userData?.playbackPositionTicks ?? 0
|
||||||
|
|
||||||
|
@ -261,28 +284,62 @@ extension VLCPlayerViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: setupViewModelListeners
|
||||||
|
|
||||||
|
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
|
||||||
|
viewModel.$playbackSpeed.sink { newSpeed in
|
||||||
|
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$screenFilled.sink { shouldFill in
|
||||||
|
self.changeFill(to: shouldFill)
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
|
||||||
|
if sliderIsScrubbing {
|
||||||
|
self.didBeginScrubbing()
|
||||||
|
} else {
|
||||||
|
self.didEndScrubbing(position: self.viewModel.sliderPercentage)
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
|
||||||
|
self.didSelectAudioStream(index: newAudioStreamIndex)
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
|
||||||
|
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Show/Hide Overlay
|
// MARK: Show/Hide Overlay
|
||||||
extension VLCPlayerViewController {
|
extension VLCPlayerViewController {
|
||||||
|
|
||||||
private func showOverlay() {
|
private func showOverlay() {
|
||||||
|
guard let overlayHostingController = currentOverlayHostingController else { return }
|
||||||
|
|
||||||
guard overlayHostingController.view.alpha != 1 else { return }
|
guard overlayHostingController.view.alpha != 1 else { return }
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.overlayHostingController.view.alpha = 1
|
overlayHostingController.view.alpha = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hideOverlay() {
|
private func hideOverlay() {
|
||||||
|
guard let overlayHostingController = currentOverlayHostingController else { return }
|
||||||
|
|
||||||
guard overlayHostingController.view.alpha != 0 else { return }
|
guard overlayHostingController.view.alpha != 0 else { return }
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.overlayHostingController.view.alpha = 0
|
overlayHostingController.view.alpha = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleOverlay() {
|
private func toggleOverlay() {
|
||||||
|
guard let overlayHostingController = currentOverlayHostingController else { return }
|
||||||
|
|
||||||
if overlayHostingController.view.alpha < 1 {
|
if overlayHostingController.view.alpha < 1 {
|
||||||
showOverlay()
|
showOverlay()
|
||||||
} else {
|
} else {
|
||||||
|
@ -464,4 +521,14 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
||||||
|
|
||||||
self.lastProgressReportTicks = currentPlayerTicks
|
self.lastProgressReportTicks = currentPlayerTicks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didSelectPreviousItem() {
|
||||||
|
setupMediaPlayer(newViewModel: viewModel.previousItemVideoPlayerViewModel!)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSelectNextItem() {
|
||||||
|
setupMediaPlayer(newViewModel: viewModel.nextItemVideoPlayerViewModel!)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,8 @@ extension BaseItemDto {
|
||||||
subtitlesEnabled: defaultAudioStream?.index != nil,
|
subtitlesEnabled: defaultAudioStream?.index != nil,
|
||||||
sliderPercentage: (self.userData?.playedPercentage ?? 0) / 100,
|
sliderPercentage: (self.userData?.playedPercentage ?? 0) / 100,
|
||||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1)
|
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||||
|
showAdjacentItems: true)
|
||||||
|
|
||||||
return videoPlayerViewModel
|
return videoPlayerViewModel
|
||||||
})
|
})
|
||||||
|
|
|
@ -37,6 +37,9 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
@Published var sliderIsScrubbing: Bool = false
|
@Published var sliderIsScrubbing: Bool = false
|
||||||
@Published var selectedAudioStreamIndex: Int
|
@Published var selectedAudioStreamIndex: Int
|
||||||
@Published var selectedSubtitleStreamIndex: Int
|
@Published var selectedSubtitleStreamIndex: Int
|
||||||
|
@Published var showAdjacentItems: Bool
|
||||||
|
@Published var previousItemVideoPlayerViewModel: VideoPlayerViewModel?
|
||||||
|
@Published var nextItemVideoPlayerViewModel: VideoPlayerViewModel?
|
||||||
|
|
||||||
let item: BaseItemDto
|
let item: BaseItemDto
|
||||||
let title: String
|
let title: String
|
||||||
|
@ -67,6 +70,8 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
// Necessary PassthroughSubject to capture manual scrubbing from sliders
|
// Necessary PassthroughSubject to capture manual scrubbing from sliders
|
||||||
let sliderScrubbingSubject = PassthroughSubject<VideoPlayerViewModel, Never>()
|
let sliderScrubbingSubject = PassthroughSubject<VideoPlayerViewModel, Never>()
|
||||||
|
|
||||||
|
// MARK: init
|
||||||
|
|
||||||
init(item: BaseItemDto,
|
init(item: BaseItemDto,
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String?,
|
subtitle: String?,
|
||||||
|
@ -83,7 +88,8 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
subtitlesEnabled: Bool,
|
subtitlesEnabled: Bool,
|
||||||
sliderPercentage: Double,
|
sliderPercentage: Double,
|
||||||
selectedAudioStreamIndex: Int,
|
selectedAudioStreamIndex: Int,
|
||||||
selectedSubtitleStreamIndex: Int) {
|
selectedSubtitleStreamIndex: Int,
|
||||||
|
showAdjacentItems: Bool) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.title = title
|
self.title = title
|
||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
|
@ -101,6 +107,7 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
self.sliderPercentage = sliderPercentage
|
self.sliderPercentage = sliderPercentage
|
||||||
self.selectedAudioStreamIndex = selectedAudioStreamIndex
|
self.selectedAudioStreamIndex = selectedAudioStreamIndex
|
||||||
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
|
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
|
||||||
|
self.showAdjacentItems = showAdjacentItems
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
@ -132,7 +139,87 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
|
|
||||||
return timeText
|
return timeText
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Adjacent Items
|
||||||
|
extension VideoPlayerViewModel {
|
||||||
|
|
||||||
|
func getAdjacentEpisodes() {
|
||||||
|
guard let seriesID = item.seriesId, item.itemType == .episode else { return }
|
||||||
|
|
||||||
|
TvShowsAPI.getEpisodes(seriesId: seriesID,
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
adjacentTo: item.id,
|
||||||
|
limit: 3)
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
print(completion)
|
||||||
|
}, receiveValue: { response in
|
||||||
|
|
||||||
|
// 4 possible states:
|
||||||
|
// 1 - only current episode
|
||||||
|
// 2 - two episodes with next episode
|
||||||
|
// 3 - two episodes with previous episode
|
||||||
|
// 4 - three episodes with current in middle
|
||||||
|
|
||||||
|
// State 1
|
||||||
|
guard let items = response.items, items.count > 1 else { return }
|
||||||
|
|
||||||
|
if items.count == 2 {
|
||||||
|
if items[0].id == self.item.id {
|
||||||
|
// State 2
|
||||||
|
let nextItem = items[1]
|
||||||
|
|
||||||
|
nextItem.createVideoPlayerViewModel()
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
} receiveValue: { videoPlayerViewModel in
|
||||||
|
self.nextItemVideoPlayerViewModel = videoPlayerViewModel
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
} else {
|
||||||
|
// State 3
|
||||||
|
let previousItem = items[0]
|
||||||
|
|
||||||
|
previousItem.createVideoPlayerViewModel()
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
} receiveValue: { videoPlayerViewModel in
|
||||||
|
self.previousItemVideoPlayerViewModel = videoPlayerViewModel
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// State 4
|
||||||
|
|
||||||
|
let previousItem = items[0]
|
||||||
|
let nextItem = items[2]
|
||||||
|
|
||||||
|
previousItem.createVideoPlayerViewModel()
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
} receiveValue: { videoPlayerViewModel in
|
||||||
|
self.previousItemVideoPlayerViewModel = videoPlayerViewModel
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
|
nextItem.createVideoPlayerViewModel()
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
} receiveValue: { videoPlayerViewModel in
|
||||||
|
self.nextItemVideoPlayerViewModel = videoPlayerViewModel
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Reports
|
||||||
|
extension VideoPlayerViewModel {
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: sendPlayReport
|
||||||
func sendPlayReport() {
|
func sendPlayReport() {
|
||||||
|
|
||||||
self.startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000
|
self.startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000
|
||||||
|
@ -168,6 +255,7 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: sendPauseReport
|
||||||
func sendPauseReport(paused: Bool) {
|
func sendPauseReport(paused: Bool) {
|
||||||
let startInfo = PlaybackStartInfo(canSeek: true,
|
let startInfo = PlaybackStartInfo(canSeek: true,
|
||||||
item: item,
|
item: item,
|
||||||
|
@ -200,6 +288,7 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: sendProgressReport
|
||||||
func sendProgressReport() {
|
func sendProgressReport() {
|
||||||
|
|
||||||
let progressInfo = PlaybackProgressInfo(canSeek: true,
|
let progressInfo = PlaybackProgressInfo(canSeek: true,
|
||||||
|
@ -232,6 +321,7 @@ final class VideoPlayerViewModel: ViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: sendStopReport
|
||||||
func sendStopReport() {
|
func sendStopReport() {
|
||||||
|
|
||||||
let stopInfo = PlaybackStopInfo(item: item,
|
let stopInfo = PlaybackStopInfo(item: item,
|
||||||
|
|
Loading…
Reference in New Issue