jellyflood/Shared/ViewModels/VideoPlayerViewModel.swift

136 lines
4.9 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 (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import Factory
import Files
import Foundation
import JellyfinAPI
import UIKit
import VLCUI
final class VideoPlayerViewModel: ViewModel {
let playbackURL: URL
let item: BaseItemDto
let mediaSource: MediaSourceInfo
let playSessionID: String
let videoStreams: [MediaStream]
let audioStreams: [MediaStream]
let subtitleStreams: [MediaStream]
let selectedAudioStreamIndex: Int
let selectedSubtitleStreamIndex: Int
let chapters: [ChapterInfo.FullInfo]
let playMethod: PlayMethod
var hlsPlaybackURL: URL {
let parameters = Paths.GetMasterHlsVideoPlaylistParameters(
isStatic: true,
tag: mediaSource.eTag,
playSessionID: playSessionID,
segmentContainer: MediaContainer.mp4.rawValue,
minSegments: 2,
mediaSourceID: mediaSource.id!,
deviceID: UIDevice.vendorUUIDString,
audioCodec: mediaSource.audioStreams?
.compactMap(\.codec)
.joined(separator: ","),
isBreakOnNonKeyFrames: true,
requireAvc: false,
transcodingMaxAudioChannels: 8,
videoCodec: videoStreams
.compactMap(\.codec)
.joined(separator: ","),
videoStreamIndex: videoStreams.first?.index,
enableAdaptiveBitrateStreaming: true
)
let request = Paths.getMasterHlsVideoPlaylist(
itemID: item.id!,
parameters: parameters
)
// TODO: don't force unwrap
let hlsStreamComponents = URLComponents(url: userSession.client.fullURL(with: request)!, resolvingAgainstBaseURL: false)!
.addingQueryItem(key: "api_key", value: userSession.user.accessToken)
return hlsStreamComponents.url!
}
// TODO: should start time be from the media source instead?
var vlcVideoPlayerConfiguration: VLCVideoPlayer.Configuration {
let configuration = VLCVideoPlayer.Configuration(url: playbackURL)
configuration.autoPlay = true
configuration.startTime = .seconds(max(0, item.startTimeSeconds - Defaults[.VideoPlayer.resumeOffset]))
if !self.audioStreams.isEmpty, self.audioStreams[0].path != nil {
configuration.audioIndex = .absolute(selectedAudioStreamIndex)
}
configuration.subtitleIndex = .absolute(selectedSubtitleStreamIndex)
configuration.subtitleSize = .absolute(Defaults[.VideoPlayer.Subtitle.subtitleSize])
configuration.subtitleColor = .absolute(Defaults[.VideoPlayer.Subtitle.subtitleColor].uiColor)
if let font = UIFont(name: Defaults[.VideoPlayer.Subtitle.subtitleFontName], size: 0) {
configuration.subtitleFont = .absolute(font)
}
configuration.playbackChildren = subtitleStreams
.filter { $0.deliveryMethod == .external }
.compactMap(\.asPlaybackChild)
return configuration
}
init(
playbackURL: URL,
item: BaseItemDto,
mediaSource: MediaSourceInfo,
playSessionID: String,
// TODO: Remove?
videoStreams: [MediaStream],
audioStreams: [MediaStream],
subtitleStreams: [MediaStream],
// <- End of Potential Remove?
selectedAudioStreamIndex: Int,
selectedSubtitleStreamIndex: Int,
chapters: [ChapterInfo.FullInfo],
playMethod: PlayMethod
) {
self.item = item
self.mediaSource = mediaSource
self.playSessionID = playSessionID
self.playbackURL = playbackURL
guard let mediaStreams = mediaSource.mediaStreams else {
fatalError("Media source does not have any streams")
}
let adjustedStreams = mediaStreams.adjustedTrackIndexes(for: playMethod, selectedAudioStreamIndex: selectedAudioStreamIndex)
self.videoStreams = adjustedStreams.filter { $0.type == MediaStreamType.video }
self.audioStreams = adjustedStreams.filter { $0.type == MediaStreamType.audio }
self.subtitleStreams = adjustedStreams.filter { $0.type == MediaStreamType.subtitle }
self.selectedAudioStreamIndex = selectedAudioStreamIndex
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
self.chapters = chapters
self.playMethod = playMethod
super.init()
}
func chapter(from seconds: Int) -> ChapterInfo.FullInfo? {
chapters.first(where: { $0.secondsRange.contains(seconds) })
}
}
extension VideoPlayerViewModel: Equatable {
static func == (lhs: VideoPlayerViewModel, rhs: VideoPlayerViewModel) -> Bool {
lhs.item == rhs.item &&
lhs.playbackURL == rhs.playbackURL
}
}