diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx
index 3044b5d3..81142e54 100644
--- a/components/ItemContent.tsx
+++ b/components/ItemContent.tsx
@@ -34,6 +34,7 @@ import { ItemHeader } from "./ItemHeader";
import { MediaSourceSelector } from "./MediaSourceSelector";
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
import { SubtitleHelper } from "@/utils/SubtitleHelper";
+import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
export type SelectedOptions = {
bitrate: Bitrate;
@@ -258,7 +259,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
)}
-
+
+
+
{item.Type !== "Program" && (
<>
{item.Type === "Episode" && (
diff --git a/components/ItemTechnicalDetails.tsx b/components/ItemTechnicalDetails.tsx
new file mode 100644
index 00000000..2b0785a9
--- /dev/null
+++ b/components/ItemTechnicalDetails.tsx
@@ -0,0 +1,236 @@
+import { Ionicons } from "@expo/vector-icons";
+import {
+ MediaSourceInfo,
+ type MediaStream,
+} from "@jellyfin/sdk/lib/generated-client";
+import React, { useMemo, useRef } from "react";
+import { TouchableOpacity, View } from "react-native";
+import { Badge } from "./Badge";
+import { Text } from "./common/Text";
+import {
+ BottomSheetModal,
+ BottomSheetBackdropProps,
+ BottomSheetBackdrop,
+ BottomSheetView,
+ BottomSheetScrollView,
+} from "@gorhom/bottom-sheet";
+import { Button } from "./Button";
+
+interface Props {
+ source?: MediaSourceInfo;
+}
+
+export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => {
+ const bottomSheetModalRef = useRef(null);
+
+ return (
+
+ Video
+ bottomSheetModalRef.current?.present()}>
+
+
+
+ More details
+
+ (
+
+ )}
+ >
+
+
+
+ Video
+
+
+
+
+
+
+ Audio
+ stream.Type === "Audio"
+ ) || []
+ }
+ />
+
+
+
+ Subtitles
+ stream.Type === "Subtitle"
+ ) || []
+ }
+ />
+
+
+
+
+
+ );
+};
+
+const SubtitleStreamInfo = ({
+ subtitleStreams,
+}: {
+ subtitleStreams: MediaStream[];
+}) => {
+ return (
+
+ {subtitleStreams.map((stream, index) => (
+
+
+ {stream.DisplayTitle}
+
+
+
+ }
+ text={stream.Language}
+ />
+
+ }
+ />
+
+
+ ))}
+
+ );
+};
+
+const AudioStreamInfo = ({ audioStreams }: { audioStreams: MediaStream[] }) => {
+ return (
+
+ {audioStreams.map((audioStreams, index) => (
+
+
+ {audioStreams.DisplayTitle}
+
+
+
+ }
+ text={audioStreams.Language}
+ />
+
+ }
+ text={audioStreams.Codec}
+ />
+ }
+ text={audioStreams.ChannelLayout}
+ />
+
+ }
+ text={formatBitrate(audioStreams.BitRate)}
+ />
+
+
+ ))}
+
+ );
+};
+
+const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => {
+ if (!source) return null;
+
+ const videoStream = useMemo(() => {
+ return source.MediaStreams?.find(
+ (stream) => stream.Type === "Video"
+ ) as MediaStream;
+ }, [source.MediaStreams]);
+
+ return (
+
+ }
+ text={formatFileSize(source.Size)}
+ />
+ }
+ text={`${videoStream.Width}x${videoStream.Height}`}
+ />
+
+ }
+ text={videoStream.VideoRange}
+ />
+
+ }
+ text={videoStream.Codec}
+ />
+
+ }
+ text={formatBitrate(videoStream.BitRate)}
+ />
+ }
+ text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`}
+ />
+
+ );
+};
+
+const formatFileSize = (bytes?: number | null) => {
+ if (!bytes) return "N/A";
+
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
+ if (bytes === 0) return "0 Byte";
+ const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
+ return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i];
+};
+
+const formatBitrate = (bitrate?: number | null) => {
+ if (!bitrate) return "N/A";
+
+ const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"];
+ if (bitrate === 0) return "0 bps";
+ const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString());
+ return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i];
+};