begin matching subtitle and audio streams among adjacent items

This commit is contained in:
Ethan Pippin 2021-12-29 15:25:50 -07:00
parent fe0c8ee03b
commit bc542dad8d
8 changed files with 104 additions and 54 deletions

View File

@ -23,7 +23,7 @@ protocol PlayerOverlayDelegate {
func didGenerallyTap()
func didBeginScrubbing()
func didEndScrubbing(position: Double)
func didEndScrubbing()
func didSelectAudioStream(index: Int)
func didSelectSubtitleStream(index: Int)

View File

@ -21,7 +21,7 @@ struct ItemLandscapeMainView: View {
// MARK: Sidebar Image
VStack {
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 130),
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130),
bh: viewModel.item.getPrimaryImageBlurHash())
.frame(width: 130, height: 195)
.cornerRadius(10)
@ -44,7 +44,8 @@ struct ItemLandscapeMainView: View {
.frame(width: 130, height: 40)
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
.cornerRadius(10)
}.disabled(viewModel.playButtonItem == nil)
}
.disabled(viewModel.playButtonItem == nil || viewModel.itemVideoPlayerViewModel == nil)
Spacer()
}

View File

@ -117,6 +117,8 @@ struct VLCPlayerCompactOverlayView: View, VideoPlayerOverlay {
Image(systemName: "captions.bubble")
}
}
.disabled(viewModel.selectedSubtitleStreamIndex == -1)
.foregroundColor(viewModel.selectedSubtitleStreamIndex == -1 ? .gray : .white)
}
// MARK: Settings Menu

View File

@ -23,7 +23,7 @@ protocol PlayerOverlayDelegate {
func didGenerallyTap()
func didBeginScrubbing()
func didEndScrubbing(position: Double)
func didEndScrubbing()
func didSelectAudioStream(index: Int)
func didSelectSubtitleStream(index: Int)

View File

@ -27,7 +27,7 @@ class VLCPlayerViewController: UIViewController {
private var vlcMediaPlayer = VLCMediaPlayer()
private var lastPlayerTicks: Int64 = 0
private var lastProgressReportTicks: Int64 = 0
private var cancellables = Set<AnyCancellable>()
private var viewModelReactCancellables = Set<AnyCancellable>()
private var overlayDismissTimer: Timer?
private var currentPlayerTicks: Int64 {
@ -229,17 +229,13 @@ extension VLCPlayerViewController {
stopOverlayDismissTimer()
// UX improvement
(vlcMediaPlayer.drawable as! UIView).isHidden = true
// Stop current media if there is one
if vlcMediaPlayer.media != nil {
cancellables.forEach({ $0.cancel() })
viewModelReactCancellables.forEach({ $0.cancel() })
vlcMediaPlayer.stop()
viewModel.sendStopReport()
viewModel.playerOverlayDelegate = nil
vlcMediaPlayer.media = nil
}
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
@ -257,32 +253,27 @@ extension VLCPlayerViewController {
newViewModel.getAdjacentEpisodes()
newViewModel.playerOverlayDelegate = self
let startPercentage = viewModel.item.userData?.playedPercentage ?? 0
if startPercentage > 0 {
newViewModel.sliderPercentage = startPercentage / 100
}
didSelectSubtitleStream(index: newViewModel.selectedSubtitleStreamIndex)
didSelectAudioStream(index: newViewModel.selectedAudioStreamIndex)
viewModel = newViewModel
}
// MARK: startPlayback
func startPlayback() {
// UX improvement
(vlcMediaPlayer.drawable as! UIView).isHidden = false
vlcMediaPlayer.play()
setMediaPlayerTimeAtCurrentSlider()
viewModel.sendPlayReport()
restartOverlayDismissTimer()
// 1 second = 10,000,000 ticks
let startTicks: Int64 = viewModel.item.userData?.playbackPositionTicks ?? 0
if startTicks != 0 {
let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000)
let secondsScrubbedTo = startTicks / 10_000_000
let offset = secondsScrubbedTo - Int64(videoPosition)
if offset > 0 {
vlcMediaPlayer.jumpForward(Int32(offset))
} else {
vlcMediaPlayer.jumpBackward(Int32(abs(offset)))
}
}
}
// MARK: setupViewModelListeners
@ -290,27 +281,42 @@ extension VLCPlayerViewController {
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
viewModel.$playbackSpeed.sink { newSpeed in
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &cancellables)
}.store(in: &viewModelReactCancellables)
viewModel.$screenFilled.sink { shouldFill in
self.changeFill(to: shouldFill)
}.store(in: &cancellables)
}.store(in: &viewModelReactCancellables)
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
if sliderIsScrubbing {
self.didBeginScrubbing()
} else {
self.didEndScrubbing(position: self.viewModel.sliderPercentage)
self.didEndScrubbing()
}
}.store(in: &cancellables)
}.store(in: &viewModelReactCancellables)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &cancellables)
}.store(in: &viewModelReactCancellables)
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
}.store(in: &cancellables)
}.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)))
}
}
}
@ -386,13 +392,15 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
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
}
lastPlayerTicks = currentPlayerTicks
// Send progress report every 5 seconds
if abs(lastProgressReportTicks - currentPlayerTicks) >= 500_000_000 {
viewModel.sendProgressReport()
@ -438,7 +446,7 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
viewModel.subtitlesEnabled = !viewModel.subtitlesEnabled
if viewModel.subtitlesEnabled {
vlcMediaPlayer.currentVideoSubTitleIndex = vlcMediaPlayer.videoSubTitlesIndexes[1] as! Int32
vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex)
} else {
vlcMediaPlayer.currentVideoSubTitleIndex = -1
}
@ -501,19 +509,8 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
stopOverlayDismissTimer()
}
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)
let newPositionOffset = secondsScrubbedTo - videoPosition
if newPositionOffset > 0 {
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
} else {
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
}
func didEndScrubbing() {
setMediaPlayerTimeAtCurrentSlider()
restartOverlayDismissTimer()

View File

@ -79,9 +79,18 @@ extension BaseItemDto {
hlsURL.addQueryItem(name: "SubtitleStreamIndex", value: "\(defaultSubtitleStream!.index!)")
}
var subtitle: String? = nil
// TODO: other forms of media subtitle
if self.itemType == .episode {
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
subtitle = "\(seriesName) - \(episodeLocator)"
}
}
let videoPlayerViewModel = VideoPlayerViewModel(item: self,
title: self.name!,
subtitle: self.seriesName,
subtitle: subtitle,
streamURL: streamURL.url!,
hlsURL: hlsURL.url!,
response: response,
@ -92,7 +101,7 @@ extension BaseItemDto {
playerState: .playing,
shouldShowGoogleCast: false,
shouldShowAirplay: false,
subtitlesEnabled: defaultAudioStream?.index != nil,
subtitlesEnabled: defaultSubtitleStream?.index != nil,
sliderPercentage: (self.userData?.playedPercentage ?? 0) / 100,
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,

View File

@ -30,7 +30,7 @@ extension Defaults.Keys {
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.suite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .thirty, suite: SwiftfinStore.Defaults.suite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .thirty, suite: SwiftfinStore.Defaults.suite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.suite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.suite)
static let nativeVideoPlayer = Key<Bool>("nativeVideoPlayer", default: false, suite: SwiftfinStore.Defaults.suite)
}

View File

@ -35,8 +35,18 @@ final class VideoPlayerViewModel: ViewModel {
}
}
@Published var sliderIsScrubbing: Bool = false
@Published var selectedAudioStreamIndex: Int
@Published var selectedSubtitleStreamIndex: Int
@Published var selectedAudioStreamIndex: Int {
didSet {
previousItemVideoPlayerViewModel?.matchAudioStream(with: self)
nextItemVideoPlayerViewModel?.matchAudioStream(with: self)
}
}
@Published var selectedSubtitleStreamIndex: Int {
didSet {
previousItemVideoPlayerViewModel?.matchSubtitleStream(with: self)
nextItemVideoPlayerViewModel?.matchSubtitleStream(with: self)
}
}
@Published var showAdjacentItems: Bool
@Published var previousItemVideoPlayerViewModel: VideoPlayerViewModel?
@Published var nextItemVideoPlayerViewModel: VideoPlayerViewModel?
@ -173,6 +183,9 @@ extension VideoPlayerViewModel {
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { videoPlayerViewModel in
videoPlayerViewModel.matchSubtitleStream(with: self)
videoPlayerViewModel.matchAudioStream(with: self)
self.nextItemVideoPlayerViewModel = videoPlayerViewModel
}
.store(in: &self.cancellables)
@ -184,6 +197,9 @@ extension VideoPlayerViewModel {
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { videoPlayerViewModel in
videoPlayerViewModel.matchSubtitleStream(with: self)
videoPlayerViewModel.matchAudioStream(with: self)
self.previousItemVideoPlayerViewModel = videoPlayerViewModel
}
.store(in: &self.cancellables)
@ -198,6 +214,9 @@ extension VideoPlayerViewModel {
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { videoPlayerViewModel in
videoPlayerViewModel.matchSubtitleStream(with: self)
videoPlayerViewModel.matchAudioStream(with: self)
self.previousItemVideoPlayerViewModel = videoPlayerViewModel
}
.store(in: &self.cancellables)
@ -206,6 +225,9 @@ extension VideoPlayerViewModel {
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { videoPlayerViewModel in
videoPlayerViewModel.matchSubtitleStream(with: self)
videoPlayerViewModel.matchAudioStream(with: self)
self.nextItemVideoPlayerViewModel = videoPlayerViewModel
}
.store(in: &self.cancellables)
@ -213,6 +235,25 @@ extension VideoPlayerViewModel {
})
.store(in: &cancellables)
}
private func matchSubtitleStream(with masterViewModel: VideoPlayerViewModel) {
guard let currentSubtitleStream = masterViewModel.subtitleStreams.first(where: { $0.index == masterViewModel.selectedSubtitleStreamIndex }) else { return }
guard let matchingSubtitleStream = self.subtitleStreams.first(where: { mediaStreamAboutEqual($0, currentSubtitleStream) }) else { return }
self.subtitlesEnabled = masterViewModel.subtitlesEnabled
self.selectedSubtitleStreamIndex = matchingSubtitleStream.index ?? -1
}
private func matchAudioStream(with masterViewModel: VideoPlayerViewModel) {
guard let currentAudioStream = masterViewModel.audioStreams.first(where: { $0.index == masterViewModel.selectedAudioStreamIndex }),
let matchingAudioStream = self.audioStreams.first(where: { mediaStreamAboutEqual($0, currentAudioStream) }) else { return }
self.selectedAudioStreamIndex = matchingAudioStream.index ?? -1
}
private func mediaStreamAboutEqual(_ lhs: MediaStream, _ rhs: MediaStream) -> Bool {
return lhs.displayTitle == rhs.displayTitle && lhs.language == rhs.language
}
}
// MARK: Reports