From 9e546e29eebf4a712263e2ed0d0383dcd1fbcf99 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 29 Jul 2026 22:50:50 +0200 Subject: [PATCH] Label local-file video quality from mpv track info instead of Unknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Yattee/Models/MPVTrack.swift | 17 +++++++++- Yattee/Services/Player/PlayerService.swift | 7 ++++ .../Player/ExpandedPlayerSheet+Layouts.swift | 3 ++ Yattee/Views/Player/ExpandedPlayerSheet.swift | 1 + Yattee/Views/Player/MPVVideoView.swift | 1 + Yattee/Views/Player/PlayerControlsView.swift | 2 ++ .../Player/QualitySelectorView+Sections.swift | 33 +++++++++++++++++-- Yattee/Views/Player/QualitySelectorView.swift | 4 +++ Yattee/Views/Player/tvOS/TVPlayerView.swift | 1 + YatteeTests/MPVTrackTests.swift | 3 ++ 10 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Yattee/Models/MPVTrack.swift b/Yattee/Models/MPVTrack.swift index 747d51ee..520fd8df 100644 --- a/Yattee/Models/MPVTrack.swift +++ b/Yattee/Models/MPVTrack.swift @@ -31,6 +31,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable { let codec: String? let channelCount: Int? let sampleRate: Int? + let width: Int? + let height: Int? + let fps: Double? /// Identifiable across types โ€” mpv ids collide between audio/sub/video. var id: String { "\(type.rawValue):\(trackID)" } @@ -48,6 +51,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable { case codec case channelCount = "demux-channel-count" case sampleRate = "demux-samplerate" + case width = "demux-w" + case height = "demux-h" + case fps = "demux-fps" } init(from decoder: Decoder) throws { @@ -64,6 +70,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable { codec = try container.decodeIfPresent(String.self, forKey: .codec) channelCount = try container.decodeIfPresent(Int.self, forKey: .channelCount) 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( @@ -78,7 +87,10 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable { isAlbumArt: Bool = false, codec: String? = nil, channelCount: Int? = nil, - sampleRate: Int? = nil + sampleRate: Int? = nil, + width: Int? = nil, + height: Int? = nil, + fps: Double? = nil ) { self.trackID = trackID self.type = type @@ -92,6 +104,9 @@ struct MPVTrack: Equatable, Sendable, Identifiable, Decodable { self.codec = codec self.channelCount = channelCount self.sampleRate = sampleRate + self.width = width + self.height = height + self.fps = fps } /// Base language code normalized to the 2-letter form when possible, so diff --git a/Yattee/Services/Player/PlayerService.swift b/Yattee/Services/Player/PlayerService.swift index 644debbd..59599f3a 100644 --- a/Yattee/Services/Player/PlayerService.swift +++ b/Yattee/Services/Player/PlayerService.swift @@ -85,6 +85,13 @@ final class PlayerService { 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 /// (quality switch, audio-mode toggle, buffer-stall retry). private var desiredEmbeddedAudioTrackID: Int? diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index e5c0f298..43edc76c 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -866,6 +866,7 @@ extension ExpandedPlayerSheet { embeddedSubtitleTracks: playerService.embeddedSubtitleTracks, currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID, currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID, + embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack, onEmbeddedAudioTrackSelected: { trackID in playerService.selectEmbeddedAudioTrack(trackID) }, @@ -1146,6 +1147,7 @@ extension ExpandedPlayerSheet { embeddedSubtitleTracks: playerService.embeddedSubtitleTracks, currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID, currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID, + embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack, onEmbeddedAudioTrackSelected: { trackID in playerService.selectEmbeddedAudioTrack(trackID) }, @@ -1533,6 +1535,7 @@ extension ExpandedPlayerSheet { embeddedSubtitleTracks: playerService.embeddedSubtitleTracks, currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID, currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID, + embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack, onEmbeddedAudioTrackSelected: { trackID in playerService.selectEmbeddedAudioTrack(trackID) }, diff --git a/Yattee/Views/Player/ExpandedPlayerSheet.swift b/Yattee/Views/Player/ExpandedPlayerSheet.swift index 44127a53..4ec361ea 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet.swift @@ -701,6 +701,7 @@ private struct PlayerSheetsModifier: ViewModifier { embeddedSubtitleTracks: playerService.embeddedSubtitleTracks, currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID, currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID, + embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack, onStreamSelected: { stream, audioStream in onStreamSelected(stream, audioStream) }, diff --git a/Yattee/Views/Player/MPVVideoView.swift b/Yattee/Views/Player/MPVVideoView.swift index b3e9f1c6..9c3f492f 100644 --- a/Yattee/Views/Player/MPVVideoView.swift +++ b/Yattee/Views/Player/MPVVideoView.swift @@ -266,6 +266,7 @@ struct MPVVideoView: View { embeddedSubtitleTracks: playerService.embeddedSubtitleTracks, currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID, currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID, + embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack, onEmbeddedAudioTrackSelected: { trackID in playerService.selectEmbeddedAudioTrack(trackID) }, diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index b1c3645b..dbf5665f 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -96,6 +96,7 @@ struct PlayerControlsView: View { var embeddedSubtitleTracks: [MPVTrack] = [] var currentEmbeddedAudioTrackID: Int? = nil var currentEmbeddedSubtitleTrackID: Int? = nil + var embeddedVideoTrack: MPVTrack? = nil /// Callback when an embedded audio track is selected var onEmbeddedAudioTrackSelected: ((Int) -> Void)? = nil /// Callback when an embedded subtitle track is selected (nil = off) @@ -315,6 +316,7 @@ struct PlayerControlsView: View { currentAudioStream: currentAudioStream, initialTab: .video, showTabPicker: false, + embeddedVideoTrack: embeddedVideoTrack, onStreamSelected: { stream, audioStream in onStreamSelected?(stream, audioStream) }, diff --git a/Yattee/Views/Player/QualitySelectorView+Sections.swift b/Yattee/Views/Player/QualitySelectorView+Sections.swift index 45a3bae9..bbe2e36f 100644 --- a/Yattee/Views/Player/QualitySelectorView+Sections.swift +++ b/Yattee/Views/Player/QualitySelectorView+Sections.swift @@ -184,7 +184,7 @@ extension QualitySelectorView { if format == .hls || format == .dash { return format == .hls ? "HLS" : "DASH" } - return stream.qualityLabel + return displayStream(for: stream).qualityLabel } 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 private func videoStreamRow(_ stream: Stream) -> some View { let isDownloadedStream: Bool = stream.url.isFileURL @@ -704,7 +733,7 @@ extension QualitySelectorView { let isPreferredQuality: Bool = stream.resolution == preferredQuality.maxResolution VideoStreamRowView( - stream: stream, + stream: displayStream(for: stream), isSelected: isSelected, isPreferredQuality: isPreferredQuality, isDownloaded: isDownloadedStream, diff --git a/Yattee/Views/Player/QualitySelectorView.swift b/Yattee/Views/Player/QualitySelectorView.swift index 0218f793..440c96f8 100644 --- a/Yattee/Views/Player/QualitySelectorView.swift +++ b/Yattee/Views/Player/QualitySelectorView.swift @@ -34,6 +34,8 @@ struct QualitySelectorView: View { let embeddedSubtitleTracks: [MPVTrack] let currentEmbeddedAudioTrackID: Int? let currentEmbeddedSubtitleTrackID: Int? + /// The playing file's video track, used to label metadata-less local streams + let embeddedVideoTrack: MPVTrack? let onEmbeddedAudioTrackSelected: (Int) -> Void /// nil = subtitles off let onEmbeddedSubtitleTrackSelected: (Int?) -> Void @@ -181,6 +183,7 @@ struct QualitySelectorView: View { embeddedSubtitleTracks: [MPVTrack] = [], currentEmbeddedAudioTrackID: Int? = nil, currentEmbeddedSubtitleTrackID: Int? = nil, + embeddedVideoTrack: MPVTrack? = nil, onStreamSelected: @escaping (Stream, Stream?) -> Void, onCaptionSelected: @escaping (Caption?) -> Void = { _ in }, onLoadOnlineStreams: @escaping () -> Void = {}, @@ -210,6 +213,7 @@ struct QualitySelectorView: View { self.embeddedSubtitleTracks = embeddedSubtitleTracks self.currentEmbeddedAudioTrackID = currentEmbeddedAudioTrackID self.currentEmbeddedSubtitleTrackID = currentEmbeddedSubtitleTrackID + self.embeddedVideoTrack = embeddedVideoTrack self.onStreamSelected = onStreamSelected self.onCaptionSelected = onCaptionSelected self.onLoadOnlineStreams = onLoadOnlineStreams diff --git a/Yattee/Views/Player/tvOS/TVPlayerView.swift b/Yattee/Views/Player/tvOS/TVPlayerView.swift index e9057f6b..e0b51694 100644 --- a/Yattee/Views/Player/tvOS/TVPlayerView.swift +++ b/Yattee/Views/Player/tvOS/TVPlayerView.swift @@ -171,6 +171,7 @@ struct TVPlayerView: View { embeddedSubtitleTracks: playerService.embeddedSubtitleTracks, currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID, currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID, + embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack, onStreamSelected: { stream, audioStream in switchToStream(stream, audioStream: audioStream) }, diff --git a/YatteeTests/MPVTrackTests.swift b/YatteeTests/MPVTrackTests.swift index 471ecb4e..c2770090 100644 --- a/YatteeTests/MPVTrackTests.swift +++ b/YatteeTests/MPVTrackTests.swift @@ -42,6 +42,9 @@ struct MPVTrackDecodingTests { #expect(video.trackID == 1) #expect(video.isDefault) #expect(video.isSelected) + #expect(video.width == 3840) + #expect(video.height == 1600) + #expect(video.fps == 23.976) let mainAudio = tracks[1] #expect(mainAudio.type == .audio)