diff --git a/JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift b/JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift index 75ca6056..e6cb3f16 100644 --- a/JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift +++ b/JellyfinPlayer/Views/VideoPlayer/NativePlayerViewController.swift @@ -14,9 +14,9 @@ class NativePlayerViewController: AVPlayerViewController { let viewModel: VideoPlayerViewModel - var timeObserverToken: Any? + private var timeObserverToken: Any? - var lastProgressTicks: Int64 = 0 + private var lastProgressTicks: Int64 = 0 init(viewModel: VideoPlayerViewModel) { @@ -99,15 +99,14 @@ class NativePlayerViewController: AVPlayerViewController { private func play() { player?.play() - - viewModel.sendPlayReport(startTimeTicks: viewModel.item.userData?.playbackPositionTicks ?? 0) + viewModel.sendPlayReport() } private func sendProgressReport(seconds: Double) { - viewModel.sendProgressReport(ticks: Int64(seconds) * 10_000_000) + viewModel.sendProgressReport() } private func stop() { - viewModel.sendStopReport(ticks: 10_000_000) + viewModel.sendStopReport() } } diff --git a/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift index 17e45565..d76b792d 100644 --- a/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift @@ -22,6 +22,7 @@ class VLCPlayerViewController: UIViewController { private let viewModel: VideoPlayerViewModel private var vlcMediaPlayer = VLCMediaPlayer() private var lastPlayerTicks: Int64 + private var lastProgressReportTicks: Int64 private var cancellables = Set() private var overlayDismissTimer: Timer? @@ -52,6 +53,7 @@ class VLCPlayerViewController: UIViewController { self.viewModel = viewModel self.lastPlayerTicks = viewModel.item.userData?.playbackPositionTicks ?? 0 + self.lastProgressReportTicks = viewModel.item.userData?.playbackPositionTicks ?? 0 super.init(nibName: nil, bundle: nil) @@ -243,7 +245,7 @@ extension VLCPlayerViewController { func startPlayback() { vlcMediaPlayer.play() - viewModel.sendPlayReport(startTimeTicks: viewModel.item.userData?.playbackPositionTicks ?? 0) + viewModel.sendPlayReport() // 1 second = 10,000,000 ticks let startTicks: Int64 = viewModel.item.userData?.playbackPositionTicks ?? 0 @@ -327,18 +329,18 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate { viewModel.sliderPercentage = Double(vlcMediaPlayer.position) - if abs(currentPlayerTicks - lastPlayerTicks) >= 10000 { + if abs(currentPlayerTicks - lastPlayerTicks) >= 10_000 { viewModel.playerState = VLCMediaPlayerState.playing } lastPlayerTicks = currentPlayerTicks - -// if CACurrentMediaTime() - lastProgressReportTime > 5 { -// mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack -// sendProgressReport(eventName: "timeupdate") -// lastProgressReportTime = CACurrentMediaTime() -// } + + if abs(lastProgressReportTicks - currentPlayerTicks) >= 500_000_000 { + viewModel.sendProgressReport() + + lastProgressReportTicks = currentPlayerTicks + } } } @@ -361,7 +363,7 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { func didSelectClose() { vlcMediaPlayer.stop() - viewModel.sendStopReport(ticks: currentPlayerTicks) + viewModel.sendStopReport() dismiss(animated: true, completion: nil) } @@ -399,12 +401,20 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { vlcMediaPlayer.jumpBackward(jumpBackwardLength.rawValue) restartOverlayDismissTimer() + + viewModel.sendProgressReport() + + self.lastProgressReportTicks = currentPlayerTicks } func didSelectForward() { vlcMediaPlayer.jumpForward(jumpForwardLength.rawValue) restartOverlayDismissTimer() + + viewModel.sendProgressReport() + + self.lastProgressReportTicks = currentPlayerTicks } func didSelectMain() { @@ -414,9 +424,11 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { vlcMediaPlayer.play() restartOverlayDismissTimer() case .playing: + viewModel.sendPauseReport(paused: true) vlcMediaPlayer.pause() restartOverlayDismissTimer(interval: 5) case .paused: + viewModel.sendPauseReport(paused: false) vlcMediaPlayer.play() default: () } @@ -433,6 +445,8 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { } func didEndScrubbing(position: Double) { + // 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) @@ -445,5 +459,9 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { } restartOverlayDismissTimer() + + viewModel.sendProgressReport() + + self.lastProgressReportTicks = currentPlayerTicks } } diff --git a/Shared/ViewModels/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel.swift index 0c1b2177..dfb16919 100644 --- a/Shared/ViewModels/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel.swift @@ -8,15 +8,15 @@ import Combine import Foundation import JellyfinAPI +import UIKit + #if os(tvOS) import TVVLCKit #else import MobileVLCKit #endif -import Stinsen -import UIKit -final class VideoPlayerViewModel: ObservableObject { +final class VideoPlayerViewModel: ViewModel { // Manually kept state because VLCKit doesn't properly set "played" // on the VLCMediaPlayer object @@ -55,11 +55,18 @@ final class VideoPlayerViewModel: ObservableObject { // Ticks of the time the media has begun var startTimeTicks: Int64? + var currentSeconds: Double { + let videoDuration = Double(item.runTimeTicks! / 10_000_000) + return round(sliderPercentage * videoDuration) + } + + var currentSecondTicks: Int64 { + return Int64(currentSeconds) * 10_000_000 + } + // Necessary PassthroughSubject to capture manual scrubbing from sliders let sliderScrubbingSubject = PassthroughSubject() - private var cancellables = Set() - init(item: BaseItemDto, title: String, subtitle: String?, @@ -95,15 +102,16 @@ final class VideoPlayerViewModel: ObservableObject { self.selectedAudioStreamIndex = selectedAudioStreamIndex self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex + super.init() + self.sliderPercentageChanged(newValue: (item.userData?.playedPercentage ?? 0) / 100) } private func sliderPercentageChanged(newValue: Double) { let videoDuration = Double(item.runTimeTicks! / 10_000_000) - let secondsScrubbedTo = round(sliderPercentage * videoDuration) - let secondsScrubbedRemaining = videoDuration - secondsScrubbedTo + let secondsScrubbedRemaining = videoDuration - currentSeconds - leftLabelText = calculateTimeText(from: secondsScrubbedTo) + leftLabelText = calculateTimeText(from: currentSeconds) rightLabelText = calculateTimeText(from: secondsScrubbedRemaining) } @@ -125,9 +133,9 @@ final class VideoPlayerViewModel: ObservableObject { return timeText } - func sendPlayReport(startTimeTicks: Int64) { + func sendPlayReport() { - self.startTimeTicks = startTimeTicks + self.startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000 let startInfo = PlaybackStartInfo(canSeek: true, item: item, @@ -153,16 +161,46 @@ final class VideoPlayerViewModel: ObservableObject { PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) .sink { completion in - print(completion) + self.handleAPIRequestError(completion: completion) } receiveValue: { _ in print("Playback start report sent!") } .store(in: &cancellables) } - func sendProgressReport(ticks: Int64) { + func sendPauseReport(paused: Bool) { + let startInfo = PlaybackStartInfo(canSeek: true, + item: item, + itemId: item.id, + sessionId: response.playSessionId, + mediaSourceId: item.id, + audioStreamIndex: audioStreams.first(where: { $0.index! == response.mediaSources?.first?.defaultAudioStreamIndex! })?.index, + subtitleStreamIndex: subtitleStreams.first(where: { $0.index! == response.mediaSources?.first?.defaultSubtitleStreamIndex ?? -1 })?.index, + isPaused: paused, + isMuted: false, + positionTicks: currentSecondTicks, + playbackStartTimeTicks: startTimeTicks, + volumeLevel: 100, + brightness: 100, + aspectRatio: nil, + playMethod: .directPlay, + liveStreamId: nil, + playSessionId: response.playSessionId, + repeatMode: .repeatNone, + nowPlayingQueue: nil, + playlistItemId: "playlistItem0" + ) - print("Progress ticks: \(ticks)") + PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) + .sink { completion in + self.handleAPIRequestError(completion: completion) + } receiveValue: { _ in + print("Pause report sent!") + } + .store(in: &cancellables) + } + + func sendProgressReport() { let progressInfo = PlaybackProgressInfo(canSeek: true, item: item, @@ -173,7 +211,7 @@ final class VideoPlayerViewModel: ObservableObject { subtitleStreamIndex: subtitleStreams.first(where: { $0.index! == response.mediaSources?.first?.defaultSubtitleStreamIndex ?? -1 })?.index, isPaused: false, isMuted: false, - positionTicks: ticks, + positionTicks: currentSecondTicks, playbackStartTimeTicks: startTimeTicks, volumeLevel: nil, brightness: nil, @@ -187,20 +225,20 @@ final class VideoPlayerViewModel: ObservableObject { PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink { completion in - print(completion) + self.handleAPIRequestError(completion: completion) } receiveValue: { _ in print("Playback progress sent!") } .store(in: &cancellables) } - func sendStopReport(ticks: Int64) { + func sendStopReport() { let stopInfo = PlaybackStopInfo(item: item, itemId: item.id, sessionId: response.playSessionId, mediaSourceId: item.id, - positionTicks: ticks, + positionTicks: currentSecondTicks, liveStreamId: nil, playSessionId: response.playSessionId, failed: nil, @@ -210,7 +248,7 @@ final class VideoPlayerViewModel: ObservableObject { PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) .sink { completion in - print(completion) + self.handleAPIRequestError(completion: completion) } receiveValue: { _ in print("Playback stop report sent!") }