mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-02 02:22:51 +01:00
feat: adding exoplayer for HDR playback
Currently MPV doesn't support HDR via external displays. giving people the choice of HDR/limited ass sub support/SDR full sub support Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { Image } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, ScrollView, View } from "react-native";
|
||||
import { Alert, Platform, ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TVPasswordEntryModal } from "@/components/login/TVPasswordEntryModal";
|
||||
@@ -33,13 +33,16 @@ import {
|
||||
} from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
AudioTranscodeMode,
|
||||
getActiveVideoPlayer,
|
||||
InactivityTimeout,
|
||||
type MpvCacheMode,
|
||||
type MpvVoDriver,
|
||||
TVTypographyScale,
|
||||
useSettings,
|
||||
VideoPlayer,
|
||||
} from "@/utils/atoms/settings";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { scaleSize } from "@/utils/scaleSize";
|
||||
import {
|
||||
getPreviousServers,
|
||||
type SavedServer,
|
||||
@@ -262,6 +265,25 @@ export default function SettingsTV() {
|
||||
const currentVoDriver = settings.mpvVoDriver ?? "gpu-next";
|
||||
const currentLanguage = settings.preferedLanguage;
|
||||
|
||||
// Video player selection. MPV is the default; ExoPlayer is only offered
|
||||
// as an opt-in alternative on Android TV. The selector is hidden on
|
||||
// other platforms.
|
||||
const isAndroidTv = Platform.OS === "android" && Platform.isTV;
|
||||
const currentVideoPlayer = getActiveVideoPlayer(settings);
|
||||
const isMpv = currentVideoPlayer !== VideoPlayer.ExoPlayer;
|
||||
|
||||
// Shared style for the ExoPlayer / MPV limitation notes shown under the
|
||||
// selector when the respective player is active. All pixel values scaled
|
||||
// so the layout holds on 4K TVs (see utils/scaleSize.ts).
|
||||
const playerNoteStyle = {
|
||||
color: "#9CA3AF",
|
||||
fontSize: typography.callout - 2,
|
||||
marginTop: scaleSize(4),
|
||||
marginBottom: scaleSize(12),
|
||||
marginLeft: scaleSize(8),
|
||||
marginRight: scaleSize(8),
|
||||
} as const;
|
||||
|
||||
// Audio transcoding options
|
||||
const audioTranscodeModeOptions: TVOptionItem<AudioTranscodeMode>[] = useMemo(
|
||||
() => [
|
||||
@@ -391,6 +413,23 @@ export default function SettingsTV() {
|
||||
[t, currentVoDriver],
|
||||
);
|
||||
|
||||
// Video player backend options (Android TV only)
|
||||
const videoPlayerOptions: TVOptionItem<VideoPlayer>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t("home.settings.video_player.exoplayer"),
|
||||
value: VideoPlayer.ExoPlayer,
|
||||
selected: currentVideoPlayer === VideoPlayer.ExoPlayer,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.video_player.mpv"),
|
||||
value: VideoPlayer.MPV,
|
||||
selected: currentVideoPlayer === VideoPlayer.MPV,
|
||||
},
|
||||
],
|
||||
[t, currentVideoPlayer],
|
||||
);
|
||||
|
||||
// Typography scale options
|
||||
const typographyScaleOptions: TVOptionItem<TVTypographyScale>[] = useMemo(
|
||||
() => [
|
||||
@@ -522,6 +561,11 @@ export default function SettingsTV() {
|
||||
return option?.label || t("home.settings.vo_driver.gpu_next");
|
||||
}, [voDriverOptions, t]);
|
||||
|
||||
const videoPlayerLabel = useMemo(() => {
|
||||
const option = videoPlayerOptions.find((o) => o.selected);
|
||||
return option?.label || "MPV";
|
||||
}, [videoPlayerOptions]);
|
||||
|
||||
const languageLabel = useMemo(() => {
|
||||
if (!currentLanguage) return t("home.settings.languages.system");
|
||||
const option = APP_LANGUAGES.find((l) => l.value === currentLanguage);
|
||||
@@ -586,6 +630,34 @@ export default function SettingsTV() {
|
||||
|
||||
{/* Audio Section */}
|
||||
<TVSectionHeader title={t("home.settings.audio.audio_title")} />
|
||||
|
||||
{/* Video Player selector — Android TV only */}
|
||||
{isAndroidTv && (
|
||||
<>
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.video_player.title")}
|
||||
value={videoPlayerLabel}
|
||||
onPress={() =>
|
||||
showOptions({
|
||||
title: t("home.settings.video_player.title"),
|
||||
options: videoPlayerOptions,
|
||||
onSelect: (value) => updateSettings({ videoPlayer: value }),
|
||||
})
|
||||
}
|
||||
/>
|
||||
{!isMpv && (
|
||||
<Text style={playerNoteStyle}>
|
||||
{t("home.settings.video_player.exoplayer_note")}
|
||||
</Text>
|
||||
)}
|
||||
{isMpv && (
|
||||
<Text style={playerNoteStyle}>
|
||||
{t("home.settings.video_player.mpv_note")}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.audio.transcode_mode.title")}
|
||||
value={audioTranscodeLabel}
|
||||
@@ -662,20 +734,23 @@ export default function SettingsTV() {
|
||||
updateSettings({ mpvSubtitleMarginY: newValue });
|
||||
}}
|
||||
/>
|
||||
<TVSettingsOptionButton
|
||||
label='Horizontal Alignment'
|
||||
value={alignXLabel}
|
||||
onPress={() =>
|
||||
showOptions({
|
||||
title: "Horizontal Alignment",
|
||||
options: alignXOptions,
|
||||
onSelect: (value) =>
|
||||
updateSettings({
|
||||
mpvSubtitleAlignX: value as "left" | "center" | "right",
|
||||
}),
|
||||
})
|
||||
}
|
||||
/>
|
||||
{isMpv && (
|
||||
<TVSettingsOptionButton
|
||||
label='Horizontal Alignment'
|
||||
value={alignXLabel}
|
||||
// ExoPlayer follows authored cue alignment; hide on ExoPlayer.
|
||||
onPress={() =>
|
||||
showOptions({
|
||||
title: "Horizontal Alignment",
|
||||
options: alignXOptions,
|
||||
onSelect: (value) =>
|
||||
updateSettings({
|
||||
mpvSubtitleAlignX: value as "left" | "center" | "right",
|
||||
}),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TVSettingsOptionButton
|
||||
label='Vertical Alignment'
|
||||
value={alignYLabel}
|
||||
@@ -748,19 +823,24 @@ export default function SettingsTV() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Video Output Section */}
|
||||
<TVSectionHeader title={t("home.settings.vo_driver.title")} />
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.vo_driver.vo_mode")}
|
||||
value={voDriverLabel}
|
||||
onPress={() =>
|
||||
showOptions({
|
||||
title: t("home.settings.vo_driver.vo_mode"),
|
||||
options: voDriverOptions,
|
||||
onSelect: (value) => updateSettings({ mpvVoDriver: value }),
|
||||
})
|
||||
}
|
||||
/>
|
||||
{/* Video Output Section — MPV only (gpu-next/gpu is a libmpv concept) */}
|
||||
{isMpv && (
|
||||
<>
|
||||
<TVSectionHeader title={t("home.settings.vo_driver.title")} />
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.vo_driver.vo_mode")}
|
||||
value={voDriverLabel}
|
||||
onPress={() =>
|
||||
showOptions({
|
||||
title: t("home.settings.vo_driver.vo_mode"),
|
||||
options: voDriverOptions,
|
||||
onSelect: (value) => updateSettings({ mpvVoDriver: value }),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TVSettingsStepper
|
||||
label={t("home.settings.buffer.buffer_duration")}
|
||||
value={settings.mpvCacheSeconds ?? 10}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
PlaybackSpeedScope,
|
||||
updatePlaybackSpeedSettings,
|
||||
} from "@/components/video-player/controls/utils/playback-speed-settings";
|
||||
import { VideoPlayerView } from "@/components/video-player/VideoPlayerView";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { useOrientation } from "@/hooks/useOrientation";
|
||||
@@ -40,7 +41,6 @@ import {
|
||||
type MpvOnErrorEventPayload,
|
||||
type MpvOnPlaybackStateChangePayload,
|
||||
type MpvOnProgressEventPayload,
|
||||
MpvPlayerView,
|
||||
type MpvPlayerViewRef,
|
||||
type MpvVideoSource,
|
||||
} from "@/modules";
|
||||
@@ -51,7 +51,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
||||
|
||||
import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getActivePlayerType, useSettings } from "@/utils/atoms/settings";
|
||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
@@ -364,7 +364,13 @@ export default function DirectPlayerPage() {
|
||||
maxStreamingBitrate: bitrateValue,
|
||||
mediaSourceId: mediaSourceId,
|
||||
subtitleStreamIndex: subtitleIndex,
|
||||
deviceProfile: generateDeviceProfile(),
|
||||
// Match the device profile to the player that will render the
|
||||
// stream so the server picks a codec/container the player can
|
||||
// actually decode.
|
||||
deviceProfile: generateDeviceProfile({
|
||||
player: getActivePlayerType(settings),
|
||||
audioMode: settings.audioTranscodeMode,
|
||||
}),
|
||||
});
|
||||
if (!res) return null;
|
||||
const { mediaSource, sessionId, url, requiredHttpHeaders } = res;
|
||||
@@ -1277,7 +1283,7 @@ export default function DirectPlayerPage() {
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MpvPlayerView
|
||||
<VideoPlayerView
|
||||
ref={videoRef}
|
||||
source={videoSource}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
|
||||
Reference in New Issue
Block a user