begin matching subtitle and audio streams among adjacent items
This commit is contained in:
parent
fe0c8ee03b
commit
bc542dad8d
|
@ -23,7 +23,7 @@ protocol PlayerOverlayDelegate {
|
|||
func didGenerallyTap()
|
||||
|
||||
func didBeginScrubbing()
|
||||
func didEndScrubbing(position: Double)
|
||||
func didEndScrubbing()
|
||||
|
||||
func didSelectAudioStream(index: Int)
|
||||
func didSelectSubtitleStream(index: Int)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,7 +23,7 @@ protocol PlayerOverlayDelegate {
|
|||
func didGenerallyTap()
|
||||
|
||||
func didBeginScrubbing()
|
||||
func didEndScrubbing(position: Double)
|
||||
func didEndScrubbing()
|
||||
|
||||
func didSelectAudioStream(index: Int)
|
||||
func didSelectSubtitleStream(index: Int)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue