feat: playback speed options

This commit is contained in:
Fredrik Burmester
2026-01-03 19:14:20 +01:00
parent 1d8d92175a
commit 3ff7c47b7f
9 changed files with 465 additions and 1 deletions

View File

@@ -35,6 +35,7 @@ import { useVideoNavigation } from "./hooks/useVideoNavigation";
import { useVideoSlider } from "./hooks/useVideoSlider";
import { useVideoTime } from "./hooks/useVideoTime";
import { useControlsTimeout } from "./useControlsTimeout";
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
import { type AspectRatio } from "./VideoScalingModeSelector";
import { type ScaleFactor } from "./VlcZoomControl";
@@ -66,6 +67,9 @@ interface Props {
onZoomToggle?: () => void;
api?: Api | null;
downloadedFiles?: DownloadedItem[];
// Playback speed props
playbackSpeed?: number;
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
}
export const Controls: FC<Props> = ({
@@ -93,6 +97,8 @@ export const Controls: FC<Props> = ({
offline = false,
api = null,
downloadedFiles = undefined,
playbackSpeed = 1.0,
setPlaybackSpeed,
}) => {
const { settings, updateSettings } = useSettings();
const router = useRouter();
@@ -484,6 +490,8 @@ export const Controls: FC<Props> = ({
setVideoScaleFactor={setVideoScaleFactor}
isZoomedToFill={isZoomedToFill}
onZoomToggle={onZoomToggle}
playbackSpeed={playbackSpeed}
setPlaybackSpeed={setPlaybackSpeed}
/>
</Animated.View>
<Animated.View

View File

@@ -7,12 +7,14 @@ import { useRouter } from "expo-router";
import { type FC, useCallback, useState } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { PlaybackSpeedSelector } from "@/components/PlaybackSpeedSelector";
import { useHaptic } from "@/hooks/useHaptic";
import { useOrientation } from "@/hooks/useOrientation";
import { OrientationLock } from "@/packages/expo-screen-orientation";
import { useSettings, VideoPlayerIOS } from "@/utils/atoms/settings";
import { ICON_SIZES } from "./constants";
import DropdownView from "./dropdown/DropdownView";
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
import {
type AspectRatio,
AspectRatioSelector,
@@ -40,6 +42,9 @@ interface HeaderControlsProps {
// KSPlayer-specific props
isZoomedToFill?: boolean;
onZoomToggle?: () => void;
// Playback speed props
playbackSpeed?: number;
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
}
export const HeaderControls: FC<HeaderControlsProps> = ({
@@ -60,6 +65,8 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
setVideoScaleFactor,
isZoomedToFill = false,
onZoomToggle,
playbackSpeed = 1.0,
setPlaybackSpeed,
}) => {
const { settings } = useSettings();
const router = useRouter();
@@ -181,6 +188,14 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
/>
</TouchableOpacity>
)}
{/* Playback Speed Control */}
{!Platform.isTV && setPlaybackSpeed && (
<PlaybackSpeedSelector
selected={playbackSpeed}
onChange={setPlaybackSpeed}
item={item}
/>
)}
{/* VLC-specific controls: Aspect Ratio and Scale/Zoom */}
{useVlcPlayer && (
<AspectRatioSelector

View File

@@ -0,0 +1,98 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { Settings } from "@/utils/atoms/settings";
export enum PlaybackSpeedScope {
Media = "media",
Show = "show",
All = "all",
}
interface ClearConflictingSettingsResult {
readonly updatedPerMedia: Settings["playbackSpeedPerMedia"];
readonly updatedPerShow: Settings["playbackSpeedPerShow"];
}
/**
* Clears conflicting playback speed settings based on the selected scope.
*
* When setting a playback speed at a certain scope, this function removes
* any more specific settings that would override the new setting:
* - "all" scope: clears both media-specific and show-specific settings
* - "media" scope: clears show-specific settings
* - "show" scope: clears media-specific settings
*/
export const clearConflictingSettings = (
scope: PlaybackSpeedScope,
item: BaseItemDto | undefined,
perMedia: Settings["playbackSpeedPerMedia"],
perShow: Settings["playbackSpeedPerShow"],
): ClearConflictingSettingsResult => {
const updatedPerMedia = { ...perMedia };
const updatedPerShow = { ...perShow };
if (scope === "all") {
// Clear both media-specific and show-specific settings
if (item?.Id && updatedPerMedia[item.Id] !== undefined) {
delete updatedPerMedia[item.Id];
}
if (item?.SeriesId && updatedPerShow[item.SeriesId] !== undefined) {
delete updatedPerShow[item.SeriesId];
}
} else if (scope === "media") {
// Clear show-specific setting only
if (item?.SeriesId && updatedPerShow[item.SeriesId] !== undefined) {
delete updatedPerShow[item.SeriesId];
}
} else if (scope === "show") {
// Clear media-specific setting only
if (item?.Id && updatedPerMedia[item.Id] !== undefined) {
delete updatedPerMedia[item.Id];
}
}
return { updatedPerMedia, updatedPerShow };
};
/**
* Updates playback speed settings based on the selected scope and speed.
*
* This function handles both clearing conflicting settings and updating
* the appropriate setting based on the scope:
* - "all": updates the default playback speed
* - "media": sets a speed for the specific media item
* - "show": sets a speed for the entire show
*/
export const updatePlaybackSpeedSettings = (
speed: number,
scope: PlaybackSpeedScope,
item: BaseItemDto | undefined,
settings: Settings,
updateSettings: (updates: Partial<Settings>) => void,
): void => {
const { updatedPerMedia, updatedPerShow } = clearConflictingSettings(
scope,
item,
settings.playbackSpeedPerMedia,
settings.playbackSpeedPerShow,
);
if (scope === "all") {
updateSettings({
defaultPlaybackSpeed: speed,
playbackSpeedPerMedia: updatedPerMedia,
playbackSpeedPerShow: updatedPerShow,
});
} else if (scope === "media" && item?.Id) {
updatedPerMedia[item.Id] = speed;
updateSettings({
playbackSpeedPerMedia: updatedPerMedia,
playbackSpeedPerShow: updatedPerShow,
});
} else if (scope === "show" && item?.SeriesId) {
updatedPerShow[item.SeriesId] = speed;
updateSettings({
playbackSpeedPerShow: updatedPerShow,
playbackSpeedPerMedia: updatedPerMedia,
});
}
};