From be92b5d75e92f28174e9f76d3e938101997ca7be Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Thu, 22 Jan 2026 08:15:02 +0100 Subject: [PATCH] feat(player): enhance technical info overlay with codec details --- components/video-player/controls/Controls.tsx | 1 + .../video-player/controls/Controls.tv.tsx | 3 + .../controls/TechnicalInfoOverlay.tsx | 106 +++++++++++++++++- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 96dfad6b..a336143e 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -481,6 +481,7 @@ export const Controls: FC = ({ getTechnicalInfo={getTechnicalInfo} playMethod={playMethod} transcodeReasons={transcodeReasons} + mediaSource={mediaSource} /> )} = ({ getTechnicalInfo={getTechnicalInfo} playMethod={playMethod} transcodeReasons={transcodeReasons} + mediaSource={mediaSource} + currentAudioIndex={audioIndex} + currentSubtitleIndex={subtitleIndex} /> )} diff --git a/components/video-player/controls/TechnicalInfoOverlay.tsx b/components/video-player/controls/TechnicalInfoOverlay.tsx index fbd357f6..5d87e697 100644 --- a/components/video-player/controls/TechnicalInfoOverlay.tsx +++ b/components/video-player/controls/TechnicalInfoOverlay.tsx @@ -1,4 +1,12 @@ -import { type FC, memo, useCallback, useEffect, useState } from "react"; +import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client"; +import { + type FC, + memo, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { Platform, StyleSheet, Text, View } from "react-native"; import Animated, { Easing, @@ -20,6 +28,9 @@ interface TechnicalInfoOverlayProps { getTechnicalInfo: () => Promise; playMethod?: PlayMethod; transcodeReasons?: string[]; + mediaSource?: MediaSourceInfo | null; + currentSubtitleIndex?: number; + currentAudioIndex?: number; } const formatBitrate = (bitsPerSecond: number): string => { @@ -48,10 +59,51 @@ const formatCodec = (codec: string): string => { flac: "FLAC", opus: "Opus", mp3: "MP3", + // Subtitle codecs + srt: "SRT", + subrip: "SRT", + ass: "ASS", + ssa: "SSA", + webvtt: "WebVTT", + vtt: "WebVTT", + pgs: "PGS", + hdmv_pgs_subtitle: "PGS", + dvd_subtitle: "VobSub", + dvdsub: "VobSub", + mov_text: "MOV Text", + cc_dec: "CC", + eia_608: "CC", }; return codecMap[codec.toLowerCase()] || codec.toUpperCase(); }; +const formatAudioChannels = (channels: number): string => { + switch (channels) { + case 1: + return "Mono"; + case 2: + return "Stereo"; + case 6: + return "5.1"; + case 8: + return "7.1"; + default: + return `${channels}ch`; + } +}; + +const formatVideoRange = (range?: string | null): string | null => { + if (!range || range === "SDR") return null; + const rangeMap: Record = { + HDR10: "HDR10", + HDR10Plus: "HDR10+", + HLG: "HLG", + "Dolby Vision": "Dolby Vision", + DolbyVision: "Dolby Vision", + }; + return rangeMap[range] || range; +}; + const formatFps = (fps: number): string => { // Common frame rates if (Math.abs(fps - 23.976) < 0.01) return "23.976"; @@ -127,6 +179,9 @@ export const TechnicalInfoOverlay: FC = memo( getTechnicalInfo, playMethod, transcodeReasons, + mediaSource, + currentSubtitleIndex, + currentAudioIndex, }) => { const { settings } = useSettings(); const insets = useSafeAreaInsets(); @@ -134,6 +189,39 @@ export const TechnicalInfoOverlay: FC = memo( const opacity = useSharedValue(0); + // Extract stream info from media source + const streamInfo = useMemo(() => { + if (!mediaSource?.MediaStreams) return null; + + const videoStream = mediaSource.MediaStreams.find( + (s) => s.Type === "Video", + ); + const audioStream = mediaSource.MediaStreams.find( + (s) => + s.Type === "Audio" && + (currentAudioIndex !== undefined + ? s.Index === currentAudioIndex + : s.IsDefault), + ); + const subtitleStream = mediaSource.MediaStreams.find( + (s) => + s.Type === "Subtitle" && + currentSubtitleIndex !== undefined && + currentSubtitleIndex >= 0 && + s.Index === currentSubtitleIndex, + ); + + return { + container: mediaSource.Container, + videoRange: videoStream?.VideoRangeType, + bitDepth: videoStream?.BitDepth, + audioChannels: audioStream?.Channels, + audioCodecFromSource: audioStream?.Codec, + subtitleCodec: subtitleStream?.Codec, + subtitleTitle: subtitleStream?.DisplayTitle, + }; + }, [mediaSource, currentAudioIndex, currentSubtitleIndex]); + // Animate visibility based on visible prop only (stays visible regardless of controls) useEffect(() => { opacity.value = withTiming(visible ? 1 : 0, { @@ -214,6 +302,10 @@ export const TechnicalInfoOverlay: FC = memo( {info?.videoWidth && info?.videoHeight && ( {info.videoWidth}x{info.videoHeight} + {streamInfo?.bitDepth ? ` ${streamInfo.bitDepth}bit` : ""} + {formatVideoRange(streamInfo?.videoRange) + ? ` ${formatVideoRange(streamInfo?.videoRange)}` + : ""} )} {info?.videoCodec && ( @@ -223,7 +315,17 @@ export const TechnicalInfoOverlay: FC = memo( )} {info?.audioCodec && ( - Audio: {formatCodec(info.audioCodec)} + + Audio: {formatCodec(info.audioCodec)} + {streamInfo?.audioChannels + ? ` ${formatAudioChannels(streamInfo.audioChannels)}` + : ""} + + )} + {streamInfo?.subtitleCodec && ( + + Subtitle: {formatCodec(streamInfo.subtitleCodec)} + )} {(info?.videoBitrate || info?.audioBitrate) && (