mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-28 22:18:14 +00:00
feat(player): add mpv cache and buffer configuration
This commit is contained in:
@@ -20,6 +20,7 @@ import { useTVOptionModal } from "@/hooks/useTVOptionModal";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
AudioTranscodeMode,
|
||||
type MpvCacheMode,
|
||||
TVTypographyScale,
|
||||
useSettings,
|
||||
} from "@/utils/atoms/settings";
|
||||
@@ -47,6 +48,7 @@ export default function SettingsTV() {
|
||||
const currentAlignY = settings.mpvSubtitleAlignY ?? "bottom";
|
||||
const currentTypographyScale =
|
||||
settings.tvTypographyScale || TVTypographyScale.Default;
|
||||
const currentCacheMode = settings.mpvCacheEnabled ?? "auto";
|
||||
|
||||
// Audio transcoding options
|
||||
const audioTranscodeModeOptions: TVOptionItem<AudioTranscodeMode>[] = useMemo(
|
||||
@@ -138,6 +140,28 @@ export default function SettingsTV() {
|
||||
[currentAlignY],
|
||||
);
|
||||
|
||||
// Cache mode options
|
||||
const cacheModeOptions: TVOptionItem<MpvCacheMode>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t("home.settings.buffer.cache_auto"),
|
||||
value: "auto",
|
||||
selected: currentCacheMode === "auto",
|
||||
},
|
||||
{
|
||||
label: t("home.settings.buffer.cache_yes"),
|
||||
value: "yes",
|
||||
selected: currentCacheMode === "yes",
|
||||
},
|
||||
{
|
||||
label: t("home.settings.buffer.cache_no"),
|
||||
value: "no",
|
||||
selected: currentCacheMode === "no",
|
||||
},
|
||||
],
|
||||
[t, currentCacheMode],
|
||||
);
|
||||
|
||||
// Typography scale options
|
||||
const typographyScaleOptions: TVOptionItem<TVTypographyScale>[] = useMemo(
|
||||
() => [
|
||||
@@ -191,6 +215,11 @@ export default function SettingsTV() {
|
||||
return option?.label || t("home.settings.appearance.display_size_default");
|
||||
}, [typographyScaleOptions, t]);
|
||||
|
||||
const cacheModeLabel = useMemo(() => {
|
||||
const option = cacheModeOptions.find((o) => o.selected);
|
||||
return option?.label || t("home.settings.buffer.cache_auto");
|
||||
}, [cacheModeOptions, t]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#000000" }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
@@ -382,6 +411,77 @@ export default function SettingsTV() {
|
||||
"Get your free API key at opensubtitles.com/en/consumers"}
|
||||
</Text>
|
||||
|
||||
{/* Buffer Settings Section */}
|
||||
<TVSectionHeader title={t("home.settings.buffer.title")} />
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.buffer.cache_mode")}
|
||||
value={cacheModeLabel}
|
||||
onPress={() =>
|
||||
showOptions({
|
||||
title: t("home.settings.buffer.cache_mode"),
|
||||
options: cacheModeOptions,
|
||||
onSelect: (value) => updateSettings({ mpvCacheEnabled: value }),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<TVSettingsStepper
|
||||
label={t("home.settings.buffer.buffer_duration")}
|
||||
value={settings.mpvCacheSeconds ?? 10}
|
||||
onDecrease={() => {
|
||||
const newValue = Math.max(
|
||||
5,
|
||||
(settings.mpvCacheSeconds ?? 10) - 5,
|
||||
);
|
||||
updateSettings({ mpvCacheSeconds: newValue });
|
||||
}}
|
||||
onIncrease={() => {
|
||||
const newValue = Math.min(
|
||||
120,
|
||||
(settings.mpvCacheSeconds ?? 10) + 5,
|
||||
);
|
||||
updateSettings({ mpvCacheSeconds: newValue });
|
||||
}}
|
||||
formatValue={(v) => `${v}s`}
|
||||
/>
|
||||
<TVSettingsStepper
|
||||
label={t("home.settings.buffer.max_cache_size")}
|
||||
value={settings.mpvDemuxerMaxBytes ?? 150}
|
||||
onDecrease={() => {
|
||||
const newValue = Math.max(
|
||||
50,
|
||||
(settings.mpvDemuxerMaxBytes ?? 150) - 25,
|
||||
);
|
||||
updateSettings({ mpvDemuxerMaxBytes: newValue });
|
||||
}}
|
||||
onIncrease={() => {
|
||||
const newValue = Math.min(
|
||||
500,
|
||||
(settings.mpvDemuxerMaxBytes ?? 150) + 25,
|
||||
);
|
||||
updateSettings({ mpvDemuxerMaxBytes: newValue });
|
||||
}}
|
||||
formatValue={(v) => `${v} MB`}
|
||||
/>
|
||||
<TVSettingsStepper
|
||||
label={t("home.settings.buffer.max_backward_cache")}
|
||||
value={settings.mpvDemuxerMaxBackBytes ?? 50}
|
||||
onDecrease={() => {
|
||||
const newValue = Math.max(
|
||||
25,
|
||||
(settings.mpvDemuxerMaxBackBytes ?? 50) - 25,
|
||||
);
|
||||
updateSettings({ mpvDemuxerMaxBackBytes: newValue });
|
||||
}}
|
||||
onIncrease={() => {
|
||||
const newValue = Math.min(
|
||||
200,
|
||||
(settings.mpvDemuxerMaxBackBytes ?? 50) + 25,
|
||||
);
|
||||
updateSettings({ mpvDemuxerMaxBackBytes: newValue });
|
||||
}}
|
||||
formatValue={(v) => `${v} MB`}
|
||||
/>
|
||||
|
||||
{/* Appearance Section */}
|
||||
<TVSectionHeader title={t("home.settings.appearance.title")} />
|
||||
<TVSettingsOptionButton
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { GestureControls } from "@/components/settings/GestureControls";
|
||||
import { MediaProvider } from "@/components/settings/MediaContext";
|
||||
import { MediaToggles } from "@/components/settings/MediaToggles";
|
||||
import { MpvBufferSettings } from "@/components/settings/MpvBufferSettings";
|
||||
import { PlaybackControlsSettings } from "@/components/settings/PlaybackControlsSettings";
|
||||
import { ChromecastSettings } from "../../../../../../components/settings/ChromecastSettings";
|
||||
|
||||
@@ -26,6 +27,7 @@ export default function PlaybackControlsPage() {
|
||||
<MediaToggles className='mb-4' />
|
||||
<GestureControls className='mb-4' />
|
||||
<PlaybackControlsSettings />
|
||||
<MpvBufferSettings />
|
||||
</MediaProvider>
|
||||
</View>
|
||||
{!Platform.isTV && <ChromecastSettings />}
|
||||
|
||||
@@ -587,6 +587,13 @@ export default function page() {
|
||||
autoplay: true,
|
||||
initialSubtitleId,
|
||||
initialAudioId,
|
||||
// Pass cache/buffer settings from user preferences
|
||||
cacheConfig: {
|
||||
enabled: settings.mpvCacheEnabled,
|
||||
cacheSeconds: settings.mpvCacheSeconds,
|
||||
maxBytes: settings.mpvDemuxerMaxBytes,
|
||||
maxBackBytes: settings.mpvDemuxerMaxBackBytes,
|
||||
},
|
||||
};
|
||||
|
||||
// Add external subtitles only for online playback
|
||||
@@ -612,6 +619,10 @@ export default function page() {
|
||||
subtitleIndex,
|
||||
audioIndex,
|
||||
offline,
|
||||
settings.mpvCacheEnabled,
|
||||
settings.mpvCacheSeconds,
|
||||
settings.mpvDemuxerMaxBytes,
|
||||
settings.mpvDemuxerMaxBackBytes,
|
||||
]);
|
||||
|
||||
const volumeUpCb = useCallback(async () => {
|
||||
|
||||
100
components/settings/MpvBufferSettings.tsx
Normal file
100
components/settings/MpvBufferSettings.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Stepper } from "@/components/inputs/Stepper";
|
||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||
import { type MpvCacheMode, useSettings } from "@/utils/atoms/settings";
|
||||
import { Text } from "../common/Text";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
import { ListItem } from "../list/ListItem";
|
||||
|
||||
const CACHE_MODE_OPTIONS: { key: string; value: MpvCacheMode }[] = [
|
||||
{ key: "home.settings.buffer.cache_auto", value: "auto" },
|
||||
{ key: "home.settings.buffer.cache_yes", value: "yes" },
|
||||
{ key: "home.settings.buffer.cache_no", value: "no" },
|
||||
];
|
||||
|
||||
export const MpvBufferSettings: React.FC = () => {
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const cacheModeOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
options: CACHE_MODE_OPTIONS.map((option) => ({
|
||||
type: "radio" as const,
|
||||
label: t(option.key),
|
||||
value: option.value,
|
||||
selected: option.value === (settings?.mpvCacheEnabled ?? "auto"),
|
||||
onPress: () => updateSettings({ mpvCacheEnabled: option.value }),
|
||||
})),
|
||||
},
|
||||
],
|
||||
[settings?.mpvCacheEnabled, t, updateSettings],
|
||||
);
|
||||
|
||||
const currentCacheModeLabel = useMemo(() => {
|
||||
const option = CACHE_MODE_OPTIONS.find(
|
||||
(o) => o.value === (settings?.mpvCacheEnabled ?? "auto"),
|
||||
);
|
||||
return option ? t(option.key) : t("home.settings.buffer.cache_auto");
|
||||
}, [settings?.mpvCacheEnabled, t]);
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
return (
|
||||
<ListGroup title={t("home.settings.buffer.title")} className='mb-4'>
|
||||
<ListItem title={t("home.settings.buffer.cache_mode")}>
|
||||
<PlatformDropdown
|
||||
groups={cacheModeOptions}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>
|
||||
{currentCacheModeLabel}
|
||||
</Text>
|
||||
<Ionicons name='chevron-expand-sharp' size={18} color='#5A5960' />
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.buffer.cache_mode")}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title={t("home.settings.buffer.buffer_duration")}>
|
||||
<Stepper
|
||||
value={settings.mpvCacheSeconds ?? 10}
|
||||
step={5}
|
||||
min={5}
|
||||
max={120}
|
||||
onUpdate={(value) => updateSettings({ mpvCacheSeconds: value })}
|
||||
appendValue='s'
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title={t("home.settings.buffer.max_cache_size")}>
|
||||
<Stepper
|
||||
value={settings.mpvDemuxerMaxBytes ?? 150}
|
||||
step={25}
|
||||
min={50}
|
||||
max={500}
|
||||
onUpdate={(value) => updateSettings({ mpvDemuxerMaxBytes: value })}
|
||||
appendValue=' MB'
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title={t("home.settings.buffer.max_backward_cache")}>
|
||||
<Stepper
|
||||
value={settings.mpvDemuxerMaxBackBytes ?? 50}
|
||||
step={25}
|
||||
min={25}
|
||||
max={200}
|
||||
onUpdate={(value) =>
|
||||
updateSettings({ mpvDemuxerMaxBackBytes: value })
|
||||
}
|
||||
appendValue=' MB'
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
);
|
||||
};
|
||||
@@ -300,7 +300,11 @@ final class MPVLayerRenderer {
|
||||
startPosition: Double? = nil,
|
||||
externalSubtitles: [String]? = nil,
|
||||
initialSubtitleId: Int? = nil,
|
||||
initialAudioId: Int? = nil
|
||||
initialAudioId: Int? = nil,
|
||||
cacheEnabled: String? = nil,
|
||||
cacheSeconds: Int? = nil,
|
||||
demuxerMaxBytes: Int? = nil,
|
||||
demuxerMaxBackBytes: Int? = nil
|
||||
) {
|
||||
currentPreset = preset
|
||||
currentURL = url
|
||||
@@ -323,6 +327,21 @@ final class MPVLayerRenderer {
|
||||
// Stop previous playback before loading new file
|
||||
self.command(handle, ["stop"])
|
||||
self.updateHTTPHeaders(headers)
|
||||
|
||||
// Apply cache/buffer settings
|
||||
if let cacheMode = cacheEnabled {
|
||||
self.setProperty(name: "cache", value: cacheMode)
|
||||
}
|
||||
if let cacheSecs = cacheSeconds {
|
||||
self.setProperty(name: "cache-secs", value: String(cacheSecs))
|
||||
}
|
||||
if let maxBytes = demuxerMaxBytes {
|
||||
self.setProperty(name: "demuxer-max-bytes", value: "\(maxBytes)MiB")
|
||||
}
|
||||
if let maxBackBytes = demuxerMaxBackBytes {
|
||||
self.setProperty(name: "demuxer-max-back-bytes", value: "\(maxBackBytes)MiB")
|
||||
}
|
||||
|
||||
// Set start position
|
||||
if let startPos = startPosition, startPos > 0 {
|
||||
self.setProperty(name: "start", value: String(format: "%.2f", startPos))
|
||||
|
||||
@@ -29,7 +29,10 @@ public class MpvPlayerModule: Module {
|
||||
guard let source = source,
|
||||
let urlString = source["url"] as? String,
|
||||
let videoURL = URL(string: urlString) else { return }
|
||||
|
||||
|
||||
// Parse cache config if provided
|
||||
let cacheConfig = source["cacheConfig"] as? [String: Any]
|
||||
|
||||
let config = VideoLoadConfig(
|
||||
url: videoURL,
|
||||
headers: source["headers"] as? [String: String],
|
||||
@@ -37,9 +40,13 @@ public class MpvPlayerModule: Module {
|
||||
startPosition: source["startPosition"] as? Double,
|
||||
autoplay: (source["autoplay"] as? Bool) ?? true,
|
||||
initialSubtitleId: source["initialSubtitleId"] as? Int,
|
||||
initialAudioId: source["initialAudioId"] as? Int
|
||||
initialAudioId: source["initialAudioId"] as? Int,
|
||||
cacheEnabled: cacheConfig?["enabled"] as? String,
|
||||
cacheSeconds: cacheConfig?["cacheSeconds"] as? Int,
|
||||
demuxerMaxBytes: cacheConfig?["maxBytes"] as? Int,
|
||||
demuxerMaxBackBytes: cacheConfig?["maxBackBytes"] as? Int
|
||||
)
|
||||
|
||||
|
||||
view.loadVideo(config: config)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ struct VideoLoadConfig {
|
||||
var initialSubtitleId: Int?
|
||||
/// MPV audio track ID to select on start (1-based, nil to use default)
|
||||
var initialAudioId: Int?
|
||||
|
||||
/// Cache/buffer settings
|
||||
var cacheEnabled: String? // "auto", "yes", or "no"
|
||||
var cacheSeconds: Int? // Seconds of video to buffer
|
||||
var demuxerMaxBytes: Int? // Max cache size in MB
|
||||
var demuxerMaxBackBytes: Int? // Max backward cache size in MB
|
||||
|
||||
init(
|
||||
url: URL,
|
||||
headers: [String: String]? = nil,
|
||||
@@ -23,7 +28,11 @@ struct VideoLoadConfig {
|
||||
startPosition: Double? = nil,
|
||||
autoplay: Bool = true,
|
||||
initialSubtitleId: Int? = nil,
|
||||
initialAudioId: Int? = nil
|
||||
initialAudioId: Int? = nil,
|
||||
cacheEnabled: String? = nil,
|
||||
cacheSeconds: Int? = nil,
|
||||
demuxerMaxBytes: Int? = nil,
|
||||
demuxerMaxBackBytes: Int? = nil
|
||||
) {
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
@@ -32,6 +41,10 @@ struct VideoLoadConfig {
|
||||
self.autoplay = autoplay
|
||||
self.initialSubtitleId = initialSubtitleId
|
||||
self.initialAudioId = initialAudioId
|
||||
self.cacheEnabled = cacheEnabled
|
||||
self.cacheSeconds = cacheSeconds
|
||||
self.demuxerMaxBytes = demuxerMaxBytes
|
||||
self.demuxerMaxBackBytes = demuxerMaxBackBytes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,13 +164,17 @@ class MpvPlayerView: ExpoView {
|
||||
startPosition: config.startPosition,
|
||||
externalSubtitles: config.externalSubtitles,
|
||||
initialSubtitleId: config.initialSubtitleId,
|
||||
initialAudioId: config.initialAudioId
|
||||
initialAudioId: config.initialAudioId,
|
||||
cacheEnabled: config.cacheEnabled,
|
||||
cacheSeconds: config.cacheSeconds,
|
||||
demuxerMaxBytes: config.demuxerMaxBytes,
|
||||
demuxerMaxBackBytes: config.demuxerMaxBackBytes
|
||||
)
|
||||
|
||||
|
||||
if config.autoplay {
|
||||
play()
|
||||
}
|
||||
|
||||
|
||||
onLoad(["url": config.url.absoluteString])
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,17 @@ export type VideoSource = {
|
||||
initialSubtitleId?: number;
|
||||
/** MPV audio track ID to select on start (1-based) */
|
||||
initialAudioId?: number;
|
||||
/** MPV cache/buffer configuration */
|
||||
cacheConfig?: {
|
||||
/** Whether caching is enabled: "auto" (default), "yes", or "no" */
|
||||
enabled?: "auto" | "yes" | "no";
|
||||
/** Seconds of video to buffer (default: 10, range: 5-120) */
|
||||
cacheSeconds?: number;
|
||||
/** Maximum cache size in MB (default: 150, range: 50-500) */
|
||||
maxBytes?: number;
|
||||
/** Maximum backward cache size in MB (default: 50, range: 25-200) */
|
||||
maxBackBytes?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type MpvPlayerViewProps = {
|
||||
|
||||
@@ -185,6 +185,16 @@
|
||||
"rewind_length": "Rewind Length",
|
||||
"seconds_unit": "s"
|
||||
},
|
||||
"buffer": {
|
||||
"title": "Buffer Settings",
|
||||
"cache_mode": "Cache Mode",
|
||||
"cache_auto": "Auto",
|
||||
"cache_yes": "Enabled",
|
||||
"cache_no": "Disabled",
|
||||
"buffer_duration": "Buffer Duration",
|
||||
"max_cache_size": "Max Cache Size",
|
||||
"max_backward_cache": "Max Backward Cache"
|
||||
},
|
||||
"gesture_controls": {
|
||||
"gesture_controls_title": "Gesture Controls",
|
||||
"horizontal_swipe_skip": "Horizontal Swipe to Skip",
|
||||
|
||||
@@ -154,6 +154,9 @@ export enum AudioTranscodeMode {
|
||||
AllowAll = "passthrough", // Direct play all audio formats
|
||||
}
|
||||
|
||||
// MPV cache mode - controls how caching is enabled
|
||||
export type MpvCacheMode = "auto" | "yes" | "no";
|
||||
|
||||
export type Settings = {
|
||||
home?: Home | null;
|
||||
deviceProfile?: "Expo" | "Native" | "Old";
|
||||
@@ -199,6 +202,11 @@ export type Settings = {
|
||||
mpvSubtitleAlignX?: "left" | "center" | "right";
|
||||
mpvSubtitleAlignY?: "top" | "center" | "bottom";
|
||||
mpvSubtitleFontSize?: number;
|
||||
// MPV buffer/cache settings
|
||||
mpvCacheEnabled?: MpvCacheMode;
|
||||
mpvCacheSeconds?: number;
|
||||
mpvDemuxerMaxBytes?: number; // MB
|
||||
mpvDemuxerMaxBackBytes?: number; // MB
|
||||
// Gesture controls
|
||||
enableHorizontalSwipeSkip: boolean;
|
||||
enableLeftSideBrightnessSwipe: boolean;
|
||||
@@ -290,6 +298,11 @@ export const defaultValues: Settings = {
|
||||
mpvSubtitleAlignX: undefined,
|
||||
mpvSubtitleAlignY: undefined,
|
||||
mpvSubtitleFontSize: undefined,
|
||||
// MPV buffer/cache defaults
|
||||
mpvCacheEnabled: "auto",
|
||||
mpvCacheSeconds: 10,
|
||||
mpvDemuxerMaxBytes: 150, // MB
|
||||
mpvDemuxerMaxBackBytes: 50, // MB
|
||||
// Gesture controls
|
||||
enableHorizontalSwipeSkip: true,
|
||||
enableLeftSideBrightnessSwipe: true,
|
||||
|
||||
Reference in New Issue
Block a user