mirror of
https://github.com/yattee/yattee.git
synced 2026-08-05 23:01:28 +00:00
Label local-file video quality from mpv track info instead of Unknown
Streams built for local folder files carry no resolution/codec/fps metadata (unknowable before demux), so the quality selector labeled them "Unknown". Use the video track mpv reports (demux-w/h/fps, codec) to fill those fields into a display-only copy of the stream — the Video row and summary now show e.g. "720p · 30fps" with a codec badge. Selection and tap handling keep using the original stream.
This commit is contained in:
@@ -31,6 +31,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable {
|
|||||||
let codec: String?
|
let codec: String?
|
||||||
let channelCount: Int?
|
let channelCount: Int?
|
||||||
let sampleRate: Int?
|
let sampleRate: Int?
|
||||||
|
let width: Int?
|
||||||
|
let height: Int?
|
||||||
|
let fps: Double?
|
||||||
|
|
||||||
/// Identifiable across types — mpv ids collide between audio/sub/video.
|
/// Identifiable across types — mpv ids collide between audio/sub/video.
|
||||||
var id: String { "\(type.rawValue):\(trackID)" }
|
var id: String { "\(type.rawValue):\(trackID)" }
|
||||||
@@ -48,6 +51,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable {
|
|||||||
case codec
|
case codec
|
||||||
case channelCount = "demux-channel-count"
|
case channelCount = "demux-channel-count"
|
||||||
case sampleRate = "demux-samplerate"
|
case sampleRate = "demux-samplerate"
|
||||||
|
case width = "demux-w"
|
||||||
|
case height = "demux-h"
|
||||||
|
case fps = "demux-fps"
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
@@ -64,6 +70,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable {
|
|||||||
codec = try container.decodeIfPresent(String.self, forKey: .codec)
|
codec = try container.decodeIfPresent(String.self, forKey: .codec)
|
||||||
channelCount = try container.decodeIfPresent(Int.self, forKey: .channelCount)
|
channelCount = try container.decodeIfPresent(Int.self, forKey: .channelCount)
|
||||||
sampleRate = try container.decodeIfPresent(Int.self, forKey: .sampleRate)
|
sampleRate = try container.decodeIfPresent(Int.self, forKey: .sampleRate)
|
||||||
|
width = try container.decodeIfPresent(Int.self, forKey: .width)
|
||||||
|
height = try container.decodeIfPresent(Int.self, forKey: .height)
|
||||||
|
fps = try container.decodeIfPresent(Double.self, forKey: .fps)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@@ -78,7 +87,10 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable {
|
|||||||
isAlbumArt: Bool = false,
|
isAlbumArt: Bool = false,
|
||||||
codec: String? = nil,
|
codec: String? = nil,
|
||||||
channelCount: Int? = nil,
|
channelCount: Int? = nil,
|
||||||
sampleRate: Int? = nil
|
sampleRate: Int? = nil,
|
||||||
|
width: Int? = nil,
|
||||||
|
height: Int? = nil,
|
||||||
|
fps: Double? = nil
|
||||||
) {
|
) {
|
||||||
self.trackID = trackID
|
self.trackID = trackID
|
||||||
self.type = type
|
self.type = type
|
||||||
@@ -92,6 +104,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable {
|
|||||||
self.codec = codec
|
self.codec = codec
|
||||||
self.channelCount = channelCount
|
self.channelCount = channelCount
|
||||||
self.sampleRate = sampleRate
|
self.sampleRate = sampleRate
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.fps = fps
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Base language code normalized to the 2-letter form when possible, so
|
/// Base language code normalized to the 2-letter form when possible, so
|
||||||
|
|||||||
@@ -85,6 +85,13 @@ final class PlayerService {
|
|||||||
embeddedSubtitleTracks.first(where: \.isSelected)?.trackID
|
embeddedSubtitleTracks.first(where: \.isSelected)?.trackID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The video track mpv currently plays. Used to label local-file streams,
|
||||||
|
/// whose Stream carries no resolution/codec/fps metadata before demux.
|
||||||
|
var primaryEmbeddedVideoTrack: MPVTrack? {
|
||||||
|
let videoTracks = embeddedTracks.filter { $0.type == .video && !$0.isExternal && !$0.isAlbumArt }
|
||||||
|
return videoTracks.first(where: \.isSelected) ?? videoTracks.first
|
||||||
|
}
|
||||||
|
|
||||||
/// Embedded-track picks that should survive same-video reloads
|
/// Embedded-track picks that should survive same-video reloads
|
||||||
/// (quality switch, audio-mode toggle, buffer-stall retry).
|
/// (quality switch, audio-mode toggle, buffer-stall retry).
|
||||||
private var desiredEmbeddedAudioTrackID: Int?
|
private var desiredEmbeddedAudioTrackID: Int?
|
||||||
|
|||||||
@@ -866,6 +866,7 @@ extension ExpandedPlayerSheet {
|
|||||||
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
||||||
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
||||||
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
||||||
|
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
|
||||||
onEmbeddedAudioTrackSelected: { trackID in
|
onEmbeddedAudioTrackSelected: { trackID in
|
||||||
playerService.selectEmbeddedAudioTrack(trackID)
|
playerService.selectEmbeddedAudioTrack(trackID)
|
||||||
},
|
},
|
||||||
@@ -1146,6 +1147,7 @@ extension ExpandedPlayerSheet {
|
|||||||
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
||||||
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
||||||
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
||||||
|
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
|
||||||
onEmbeddedAudioTrackSelected: { trackID in
|
onEmbeddedAudioTrackSelected: { trackID in
|
||||||
playerService.selectEmbeddedAudioTrack(trackID)
|
playerService.selectEmbeddedAudioTrack(trackID)
|
||||||
},
|
},
|
||||||
@@ -1533,6 +1535,7 @@ extension ExpandedPlayerSheet {
|
|||||||
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
||||||
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
||||||
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
||||||
|
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
|
||||||
onEmbeddedAudioTrackSelected: { trackID in
|
onEmbeddedAudioTrackSelected: { trackID in
|
||||||
playerService.selectEmbeddedAudioTrack(trackID)
|
playerService.selectEmbeddedAudioTrack(trackID)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -701,6 +701,7 @@ private struct PlayerSheetsModifier: ViewModifier {
|
|||||||
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
||||||
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
||||||
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
||||||
|
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
|
||||||
onStreamSelected: { stream, audioStream in
|
onStreamSelected: { stream, audioStream in
|
||||||
onStreamSelected(stream, audioStream)
|
onStreamSelected(stream, audioStream)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ struct MPVVideoView: View {
|
|||||||
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
||||||
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
||||||
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
||||||
|
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
|
||||||
onEmbeddedAudioTrackSelected: { trackID in
|
onEmbeddedAudioTrackSelected: { trackID in
|
||||||
playerService.selectEmbeddedAudioTrack(trackID)
|
playerService.selectEmbeddedAudioTrack(trackID)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ struct PlayerControlsView: View {
|
|||||||
var embeddedSubtitleTracks: [MPVTrack] = []
|
var embeddedSubtitleTracks: [MPVTrack] = []
|
||||||
var currentEmbeddedAudioTrackID: Int? = nil
|
var currentEmbeddedAudioTrackID: Int? = nil
|
||||||
var currentEmbeddedSubtitleTrackID: Int? = nil
|
var currentEmbeddedSubtitleTrackID: Int? = nil
|
||||||
|
var embeddedVideoTrack: MPVTrack? = nil
|
||||||
/// Callback when an embedded audio track is selected
|
/// Callback when an embedded audio track is selected
|
||||||
var onEmbeddedAudioTrackSelected: ((Int) -> Void)? = nil
|
var onEmbeddedAudioTrackSelected: ((Int) -> Void)? = nil
|
||||||
/// Callback when an embedded subtitle track is selected (nil = off)
|
/// Callback when an embedded subtitle track is selected (nil = off)
|
||||||
@@ -315,6 +316,7 @@ struct PlayerControlsView: View {
|
|||||||
currentAudioStream: currentAudioStream,
|
currentAudioStream: currentAudioStream,
|
||||||
initialTab: .video,
|
initialTab: .video,
|
||||||
showTabPicker: false,
|
showTabPicker: false,
|
||||||
|
embeddedVideoTrack: embeddedVideoTrack,
|
||||||
onStreamSelected: { stream, audioStream in
|
onStreamSelected: { stream, audioStream in
|
||||||
onStreamSelected?(stream, audioStream)
|
onStreamSelected?(stream, audioStream)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ extension QualitySelectorView {
|
|||||||
if format == .hls || format == .dash {
|
if format == .hls || format == .dash {
|
||||||
return format == .hls ? "HLS" : "DASH"
|
return format == .hls ? "HLS" : "DASH"
|
||||||
}
|
}
|
||||||
return stream.qualityLabel
|
return displayStream(for: stream).qualityLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentAudioDisplayValue: String {
|
private var currentAudioDisplayValue: String {
|
||||||
@@ -695,6 +695,35 @@ extension QualitySelectorView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A local file's Stream carries no resolution/codec/fps metadata
|
||||||
|
/// (`MediaFile.toStream` cannot know them before demux), which would label
|
||||||
|
/// the row "Unknown". Once mpv reports the file's video track, fill those
|
||||||
|
/// fields for display. The enriched copy is display-only — selection and
|
||||||
|
/// tap handling keep using the original stream (compared by URL).
|
||||||
|
func displayStream(for stream: Stream) -> Stream {
|
||||||
|
guard stream.url.isFileURL,
|
||||||
|
stream.resolution == nil,
|
||||||
|
!stream.isAudioOnly,
|
||||||
|
let track = embeddedVideoTrack,
|
||||||
|
let width = track.width,
|
||||||
|
let height = track.height else {
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
return Stream(
|
||||||
|
url: stream.url,
|
||||||
|
resolution: StreamResolution(width: width, height: height),
|
||||||
|
format: stream.format,
|
||||||
|
videoCodec: track.codec ?? stream.videoCodec,
|
||||||
|
audioCodec: stream.audioCodec,
|
||||||
|
bitrate: stream.bitrate,
|
||||||
|
fileSize: stream.fileSize,
|
||||||
|
isLive: stream.isLive,
|
||||||
|
mimeType: stream.mimeType,
|
||||||
|
httpHeaders: stream.httpHeaders,
|
||||||
|
fps: track.fps.map { Int($0.rounded()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func videoStreamRow(_ stream: Stream) -> some View {
|
private func videoStreamRow(_ stream: Stream) -> some View {
|
||||||
let isDownloadedStream: Bool = stream.url.isFileURL
|
let isDownloadedStream: Bool = stream.url.isFileURL
|
||||||
@@ -704,7 +733,7 @@ extension QualitySelectorView {
|
|||||||
let isPreferredQuality: Bool = stream.resolution == preferredQuality.maxResolution
|
let isPreferredQuality: Bool = stream.resolution == preferredQuality.maxResolution
|
||||||
|
|
||||||
VideoStreamRowView(
|
VideoStreamRowView(
|
||||||
stream: stream,
|
stream: displayStream(for: stream),
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
isPreferredQuality: isPreferredQuality,
|
isPreferredQuality: isPreferredQuality,
|
||||||
isDownloaded: isDownloadedStream,
|
isDownloaded: isDownloadedStream,
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ struct QualitySelectorView: View {
|
|||||||
let embeddedSubtitleTracks: [MPVTrack]
|
let embeddedSubtitleTracks: [MPVTrack]
|
||||||
let currentEmbeddedAudioTrackID: Int?
|
let currentEmbeddedAudioTrackID: Int?
|
||||||
let currentEmbeddedSubtitleTrackID: Int?
|
let currentEmbeddedSubtitleTrackID: Int?
|
||||||
|
/// The playing file's video track, used to label metadata-less local streams
|
||||||
|
let embeddedVideoTrack: MPVTrack?
|
||||||
let onEmbeddedAudioTrackSelected: (Int) -> Void
|
let onEmbeddedAudioTrackSelected: (Int) -> Void
|
||||||
/// nil = subtitles off
|
/// nil = subtitles off
|
||||||
let onEmbeddedSubtitleTrackSelected: (Int?) -> Void
|
let onEmbeddedSubtitleTrackSelected: (Int?) -> Void
|
||||||
@@ -181,6 +183,7 @@ struct QualitySelectorView: View {
|
|||||||
embeddedSubtitleTracks: [MPVTrack] = [],
|
embeddedSubtitleTracks: [MPVTrack] = [],
|
||||||
currentEmbeddedAudioTrackID: Int? = nil,
|
currentEmbeddedAudioTrackID: Int? = nil,
|
||||||
currentEmbeddedSubtitleTrackID: Int? = nil,
|
currentEmbeddedSubtitleTrackID: Int? = nil,
|
||||||
|
embeddedVideoTrack: MPVTrack? = nil,
|
||||||
onStreamSelected: @escaping (Stream, Stream?) -> Void,
|
onStreamSelected: @escaping (Stream, Stream?) -> Void,
|
||||||
onCaptionSelected: @escaping (Caption?) -> Void = { _ in },
|
onCaptionSelected: @escaping (Caption?) -> Void = { _ in },
|
||||||
onLoadOnlineStreams: @escaping () -> Void = {},
|
onLoadOnlineStreams: @escaping () -> Void = {},
|
||||||
@@ -210,6 +213,7 @@ struct QualitySelectorView: View {
|
|||||||
self.embeddedSubtitleTracks = embeddedSubtitleTracks
|
self.embeddedSubtitleTracks = embeddedSubtitleTracks
|
||||||
self.currentEmbeddedAudioTrackID = currentEmbeddedAudioTrackID
|
self.currentEmbeddedAudioTrackID = currentEmbeddedAudioTrackID
|
||||||
self.currentEmbeddedSubtitleTrackID = currentEmbeddedSubtitleTrackID
|
self.currentEmbeddedSubtitleTrackID = currentEmbeddedSubtitleTrackID
|
||||||
|
self.embeddedVideoTrack = embeddedVideoTrack
|
||||||
self.onStreamSelected = onStreamSelected
|
self.onStreamSelected = onStreamSelected
|
||||||
self.onCaptionSelected = onCaptionSelected
|
self.onCaptionSelected = onCaptionSelected
|
||||||
self.onLoadOnlineStreams = onLoadOnlineStreams
|
self.onLoadOnlineStreams = onLoadOnlineStreams
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ struct TVPlayerView: View {
|
|||||||
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
|
||||||
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
|
||||||
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
|
||||||
|
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
|
||||||
onStreamSelected: { stream, audioStream in
|
onStreamSelected: { stream, audioStream in
|
||||||
switchToStream(stream, audioStream: audioStream)
|
switchToStream(stream, audioStream: audioStream)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ struct MPVTrackDecodingTests {
|
|||||||
#expect(video.trackID == 1)
|
#expect(video.trackID == 1)
|
||||||
#expect(video.isDefault)
|
#expect(video.isDefault)
|
||||||
#expect(video.isSelected)
|
#expect(video.isSelected)
|
||||||
|
#expect(video.width == 3840)
|
||||||
|
#expect(video.height == 1600)
|
||||||
|
#expect(video.fps == 23.976)
|
||||||
|
|
||||||
let mainAudio = tracks[1]
|
let mainAudio = tracks[1]
|
||||||
#expect(mainAudio.type == .audio)
|
#expect(mainAudio.type == .audio)
|
||||||
|
|||||||
Reference in New Issue
Block a user