302 lines
9.4 KiB
Swift
302 lines
9.4 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 Factory
|
|
import Foundation
|
|
import JellyfinAPI
|
|
import VLCUI
|
|
|
|
extension MediaStream {
|
|
|
|
static var none: MediaStream = .init(displayTitle: L10n.none, index: -1)
|
|
|
|
var asPlaybackChild: VLCVideoPlayer.PlaybackChild? {
|
|
guard let deliveryURL, let client = Container.shared.currentUserSession()?.client else { return nil }
|
|
|
|
let deliveryPath = deliveryURL.removingFirst(if: client.configuration.url.absoluteString.last == "/")
|
|
|
|
guard let fullURL = client.fullURL(with: deliveryPath) else { return nil }
|
|
|
|
return .init(
|
|
url: fullURL,
|
|
type: .subtitle,
|
|
enforce: false
|
|
)
|
|
}
|
|
|
|
var is4kVideo: Bool {
|
|
(width ?? 0) > 3800 && type == .video
|
|
}
|
|
|
|
var is51AudioChannelLayout: Bool {
|
|
channelLayout == "5.1"
|
|
}
|
|
|
|
var is71AudioChannelLayout: Bool {
|
|
channelLayout == "7.1"
|
|
}
|
|
|
|
var isHDVideo: Bool {
|
|
(width ?? 0) > 1900 && type == .video
|
|
}
|
|
|
|
// MARK: Property groups
|
|
|
|
var metadataProperties: [TextPair] {
|
|
var properties: [TextPair] = []
|
|
|
|
if let value = type {
|
|
properties.append(.init(title: "Type", subtitle: value.rawValue))
|
|
}
|
|
|
|
if let value = codec {
|
|
properties.append(.init(title: "Codec", subtitle: value))
|
|
}
|
|
|
|
if let value = codecTag {
|
|
properties.append(.init(title: "Codec Tag", subtitle: value))
|
|
}
|
|
|
|
if let value = language {
|
|
properties.append(.init(title: "Language", subtitle: value))
|
|
}
|
|
|
|
if let value = timeBase {
|
|
properties.append(.init(title: "Time Base", subtitle: value))
|
|
}
|
|
|
|
if let value = codecTimeBase {
|
|
properties.append(.init(title: "Codec Time Base", subtitle: value))
|
|
}
|
|
|
|
if let value = videoRange {
|
|
properties.append(.init(title: "Video Range", subtitle: value.rawValue))
|
|
}
|
|
|
|
if let value = isInterlaced {
|
|
properties.append(.init(title: "Interlaced", subtitle: value.description))
|
|
}
|
|
|
|
if let value = isAVC {
|
|
properties.append(.init(title: "AVC", subtitle: value.description))
|
|
}
|
|
|
|
if let value = channelLayout {
|
|
properties.append(.init(title: "Channel Layout", subtitle: value))
|
|
}
|
|
|
|
if let value = bitRate {
|
|
properties.append(.init(title: "Bitrate", subtitle: value.description))
|
|
}
|
|
|
|
if let value = bitDepth {
|
|
properties.append(.init(title: "Bit Depth", subtitle: value.description))
|
|
}
|
|
|
|
if let value = refFrames {
|
|
properties.append(.init(title: "Reference Frames", subtitle: value.description))
|
|
}
|
|
|
|
if let value = packetLength {
|
|
properties.append(.init(title: "Packet Length", subtitle: value.description))
|
|
}
|
|
|
|
if let value = channels {
|
|
properties.append(.init(title: "Channels", subtitle: value.description))
|
|
}
|
|
|
|
if let value = sampleRate {
|
|
properties.append(.init(title: "Sample Rate", subtitle: value.description))
|
|
}
|
|
|
|
if let value = isDefault {
|
|
properties.append(.init(title: "Default", subtitle: value.description))
|
|
}
|
|
|
|
if let value = isForced {
|
|
properties.append(.init(title: "Forced", subtitle: value.description))
|
|
}
|
|
|
|
if let value = averageFrameRate {
|
|
properties.append(.init(title: "Average Frame Rate", subtitle: value.description))
|
|
}
|
|
|
|
if let value = realFrameRate {
|
|
properties.append(.init(title: "Real Frame Rate", subtitle: value.description))
|
|
}
|
|
|
|
if let value = profile {
|
|
properties.append(.init(title: "Profile", subtitle: value))
|
|
}
|
|
|
|
if let value = aspectRatio {
|
|
properties.append(.init(title: "Aspect Ratio", subtitle: value))
|
|
}
|
|
|
|
if let value = index {
|
|
properties.append(.init(title: "Index", subtitle: value.description))
|
|
}
|
|
|
|
if let value = score {
|
|
properties.append(.init(title: "Score", subtitle: value.description))
|
|
}
|
|
|
|
if let value = pixelFormat {
|
|
properties.append(.init(title: "Pixel Format", subtitle: value))
|
|
}
|
|
|
|
if let value = level {
|
|
properties.append(.init(title: "Level", subtitle: value.description))
|
|
}
|
|
|
|
if let value = isAnamorphic {
|
|
properties.append(.init(title: "Anamorphic", subtitle: value.description))
|
|
}
|
|
|
|
return properties
|
|
}
|
|
|
|
var colorProperties: [TextPair] {
|
|
var properties: [TextPair] = []
|
|
|
|
if let value = colorRange {
|
|
properties.append(.init(title: "Range", subtitle: value))
|
|
}
|
|
|
|
if let value = colorSpace {
|
|
properties.append(.init(title: "Space", subtitle: value))
|
|
}
|
|
|
|
if let value = colorTransfer {
|
|
properties.append(.init(title: "Transfer", subtitle: value))
|
|
}
|
|
|
|
if let value = colorPrimaries {
|
|
properties.append(.init(title: "Primaries", subtitle: value))
|
|
}
|
|
|
|
return properties
|
|
}
|
|
|
|
var deliveryProperties: [TextPair] {
|
|
var properties: [TextPair] = []
|
|
|
|
if let value = isExternal {
|
|
properties.append(.init(title: "External", subtitle: value.description))
|
|
}
|
|
|
|
if let value = deliveryMethod {
|
|
properties.append(.init(title: "Delivery Method", subtitle: value.rawValue))
|
|
}
|
|
|
|
if let value = deliveryURL {
|
|
properties.append(.init(title: "URL", subtitle: value))
|
|
}
|
|
|
|
if let value = deliveryURL {
|
|
properties.append(.init(title: "External URL", subtitle: value.description))
|
|
}
|
|
|
|
if let value = isTextSubtitleStream {
|
|
properties.append(.init(title: "Text Subtitle", subtitle: value.description))
|
|
}
|
|
|
|
if let value = path {
|
|
properties.append(.init(title: "Path", subtitle: value))
|
|
}
|
|
|
|
return properties
|
|
}
|
|
}
|
|
|
|
extension [MediaStream] {
|
|
|
|
/// Adjusts track indexes for a full set of media streams.
|
|
/// For non-transcode stream types:
|
|
/// Internal tracks (non-external) are ordered as: Video, Audio, Subtitles, then any others.
|
|
/// Their relative order within each group is preserved and indexes start at 0.
|
|
/// For transcode stream type:
|
|
/// Only the first internal video track and the first internal audio track are included, in that order.
|
|
/// In both cases, external tracks are appended in their original order with indexes continuing after internal tracks.
|
|
func adjustedTrackIndexes(for playMethod: PlayMethod, selectedAudioStreamIndex: Int) -> [MediaStream] {
|
|
let internalTracks = self.filter { !($0.isExternal ?? false) }
|
|
let externalTracks = self.filter { $0.isExternal ?? false }
|
|
|
|
var orderedInternal: [MediaStream] = []
|
|
|
|
let subtitleInternal = internalTracks.filter { $0.type == .subtitle }
|
|
|
|
// TODO: Do we need this for other media types? I think movies/shows we only care about video, audio, and subtitles.
|
|
let otherInternal = internalTracks.filter { $0.type != .video && $0.type != .audio && $0.type != .subtitle }
|
|
|
|
if playMethod == .transcode {
|
|
// Only include the first video and first audio track for transcode.
|
|
let videoInternal = internalTracks.filter { $0.type == .video }
|
|
let audioInternal = internalTracks.filter { $0.type == .audio }
|
|
|
|
if let firstVideo = videoInternal.first {
|
|
orderedInternal.append(firstVideo)
|
|
}
|
|
if let selectedAudio = audioInternal.first(where: { $0.index == selectedAudioStreamIndex }) {
|
|
orderedInternal.append(selectedAudio)
|
|
}
|
|
|
|
orderedInternal += subtitleInternal
|
|
orderedInternal += otherInternal
|
|
} else {
|
|
let videoInternal = internalTracks.filter { $0.type == .video }
|
|
let audioInternal = internalTracks.filter { $0.type == .audio }
|
|
|
|
orderedInternal = videoInternal + audioInternal + subtitleInternal + otherInternal
|
|
}
|
|
|
|
var newInternalTracks: [MediaStream] = []
|
|
for (index, var track) in orderedInternal.enumerated() {
|
|
track.index = index
|
|
newInternalTracks.append(track)
|
|
}
|
|
|
|
var newExternalTracks: [MediaStream] = []
|
|
let startingIndexForExternal = newInternalTracks.count
|
|
for (offset, var track) in externalTracks.enumerated() {
|
|
track.index = startingIndexForExternal + offset
|
|
newExternalTracks.append(track)
|
|
}
|
|
|
|
return newInternalTracks + newExternalTracks
|
|
}
|
|
|
|
var has4KVideo: Bool {
|
|
contains { $0.is4kVideo }
|
|
}
|
|
|
|
var has51AudioChannelLayout: Bool {
|
|
contains { $0.is51AudioChannelLayout }
|
|
}
|
|
|
|
var has71AudioChannelLayout: Bool {
|
|
contains { $0.is71AudioChannelLayout }
|
|
}
|
|
|
|
var hasHDVideo: Bool {
|
|
contains { $0.isHDVideo }
|
|
}
|
|
|
|
var hasHDRVideo: Bool {
|
|
contains { $0.videoRangeType?.isHDR == true }
|
|
}
|
|
|
|
var hasDolbyVision: Bool {
|
|
contains { $0.videoRangeType?.isDolbyVision == true }
|
|
}
|
|
|
|
var hasSubtitles: Bool {
|
|
contains { $0.type == .subtitle }
|
|
}
|
|
}
|