Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
fc85893c7e chore(deps): Update dependency sonner-native to v0.23.0 2026-02-04 22:42:52 +00:00
7 changed files with 11 additions and 204 deletions

View File

@@ -28,7 +28,6 @@ import {
} from "@/components/video-player/controls/utils/playback-speed-settings";
import useRouter from "@/hooks/useAppRouter";
import { useHaptic } from "@/hooks/useHaptic";
import { useIntroPlayback } from "@/hooks/useIntroPlayback";
import { useOrientation } from "@/hooks/useOrientation";
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import usePlaybackSpeed from "@/hooks/usePlaybackSpeed";
@@ -56,7 +55,7 @@ import {
} from "@/utils/jellyfin/subtitleUtils";
import { writeToLog } from "@/utils/log";
import { generateDeviceProfile } from "@/utils/profiles/native";
import { msToTicks, ticksToMs, ticksToSeconds } from "@/utils/time";
import { msToTicks, ticksToSeconds } from "@/utils/time";
export default function page() {
const videoRef = useRef<MpvPlayerViewRef>(null);
@@ -88,8 +87,6 @@ export default function page() {
const progress = useSharedValue(0);
const isSeeking = useSharedValue(false);
const cacheProgress = useSharedValue(0);
// Track whether we've already triggered completion for the current intro
const introCompletionTriggered = useSharedValue(false);
const VolumeManager = Platform.isTV
? null
: require("react-native-volume-manager");
@@ -152,14 +149,6 @@ export default function page() {
isError: false,
});
// Intro playback hook - manages intro video playback before main content
const { intros, currentIntro, isPlayingIntro, skipAllIntros } =
useIntroPlayback({
api,
itemId: item?.Id || null,
userId: user?.Id,
});
// Resolve audio index: use URL param if provided, otherwise use stored index for offline playback
const audioIndex = useMemo(() => {
if (audioIndexFromUrl !== undefined) {
@@ -258,9 +247,6 @@ export default function page() {
isError: false,
});
// Intro stream state - separate from main content stream
const [introStream, setIntroStream] = useState<Stream | null>(null);
useEffect(() => {
const fetchStreamData = async () => {
setStreamStatus({ isLoading: true, isError: false });
@@ -341,57 +327,6 @@ export default function page() {
downloadedItem,
]);
// Fetch intro stream when current intro changes
useEffect(() => {
const fetchIntroStreamData = async () => {
// Don't fetch intro stream if offline or no current intro
if (offline || !currentIntro?.Id || !api || !user?.Id) {
setIntroStream(null);
return;
}
try {
const res = await getStreamUrl({
api,
item: currentIntro,
startTimeTicks: 0, // Always start from beginning for intros
userId: user.Id,
audioStreamIndex: audioIndex,
maxStreamingBitrate: bitrateValue,
mediaSourceId: undefined,
subtitleStreamIndex: subtitleIndex,
deviceProfile: generateDeviceProfile(),
});
if (!res) return;
const { mediaSource, sessionId, url } = res;
if (!sessionId || !mediaSource || !url) {
console.error("Failed to get intro stream URL");
return;
}
setIntroStream({ mediaSource, sessionId, url });
} catch (error) {
console.error("Failed to fetch intro stream:", error);
}
};
fetchIntroStreamData();
}, [
currentIntro,
api,
user?.Id,
audioIndex,
bitrateValue,
subtitleIndex,
offline,
]);
// Reset intro completion flag when a new intro starts playing
useEffect(() => {
if (isPlayingIntro) {
introCompletionTriggered.value = false;
}
}, [isPlayingIntro, currentIntro]);
useEffect(() => {
if (!stream || !api || offline) return;
const reportPlaybackStart = async () => {
@@ -545,21 +480,6 @@ export default function page() {
lastUrlUpdateTime.value = now;
}
// Handle intro completion - check if intro has reached its end
if (isPlayingIntro && currentIntro) {
const introDuration = ticksToMs(currentIntro.RunTimeTicks || 0);
// Check if we're near the end of the intro (within 1000ms buffer)
// Use a larger buffer to ensure reliable detection even with short intros
// or if MPV doesn't fire progress callbacks frequently
if (currentTime >= introDuration - 1000) {
// Only trigger once per intro to avoid multiple calls
if (!introCompletionTriggered.value) {
introCompletionTriggered.value = true;
skipAllIntros();
}
}
}
if (!item?.Id) return;
playbackManager.reportPlaybackProgress(
@@ -576,9 +496,6 @@ export default function page() {
isSeeking,
isPlaybackStopped,
isBuffering,
isPlayingIntro,
currentIntro,
skipAllIntros,
],
);
@@ -589,11 +506,9 @@ export default function page() {
/** Build video source config for MPV */
const videoSource = useMemo<MpvVideoSource | undefined>(() => {
// Use intro stream if playing intro, otherwise use main content stream
const activeStream = isPlayingIntro ? introStream : stream;
if (!activeStream?.url) return undefined;
if (!stream?.url) return undefined;
const mediaSource = activeStream.mediaSource;
const mediaSource = stream.mediaSource;
const isTranscoding = Boolean(mediaSource?.TranscodingUrl);
// Get external subtitle URLs
@@ -629,17 +544,14 @@ export default function page() {
);
// Calculate start position directly here to avoid timing issues
// For intros, always start from 0
const startTicks = isPlayingIntro
? 0
: playbackPositionFromUrl
? Number.parseInt(playbackPositionFromUrl, 10)
: (item?.UserData?.PlaybackPositionTicks ?? 0);
const startTicks = playbackPositionFromUrl
? Number.parseInt(playbackPositionFromUrl, 10)
: (item?.UserData?.PlaybackPositionTicks ?? 0);
const startPos = ticksToSeconds(startTicks);
// Build source config - headers only needed for online streaming
const source: MpvVideoSource = {
url: activeStream.url,
url: stream.url,
startPosition: startPos,
autoplay: true,
initialSubtitleId,
@@ -662,8 +574,6 @@ export default function page() {
}, [
stream?.url,
stream?.mediaSource,
introStream?.url,
introStream?.mediaSource,
item?.UserData?.PlaybackPositionTicks,
playbackPositionFromUrl,
api?.basePath,
@@ -671,7 +581,6 @@ export default function page() {
subtitleIndex,
audioIndex,
offline,
isPlayingIntro,
]);
const volumeUpCb = useCallback(async () => {
@@ -1084,9 +993,6 @@ export default function page() {
getTechnicalInfo={getTechnicalInfo}
playMethod={playMethod}
transcodeReasons={transcodeReasons}
isPlayingIntro={isPlayingIntro}
skipAllIntros={skipAllIntros}
intros={intros}
/>
)}
</View>

View File

@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "streamyfin",
@@ -88,7 +89,7 @@
"react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"sonner-native": "0.21.2",
"sonner-native": "0.23.0",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
"zod": "4.1.13",
@@ -1829,7 +1830,7 @@
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
"sonner-native": ["sonner-native@0.21.2", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.10.1", "react-native-safe-area-context": ">=4.10.5", "react-native-screens": ">=3.31.1", "react-native-svg": ">=15.6.0" } }, "sha512-LnGPmfgzrNIwcc+FvcLJqx8aH1dEHePRzvNR8aIR4kl9spySRkXK160GmQIazjfm6mSMlPqZwRa5eycvrzg/eQ=="],
"sonner-native": ["sonner-native@0.23.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.10.1", "react-native-safe-area-context": ">=4.10.5", "react-native-screens": ">=3.31.1", "react-native-svg": ">=15.6.0" } }, "sha512-Z6T4l6R+PVGbG00pml+i23zplHLv3hq5yCVev9Y9GoJGQTzQ2VbtnMripvuZQ32wrghmSmlKxyb8eRu5TzY7VQ=="],
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],

View File

@@ -57,11 +57,6 @@ interface BottomControlsProps {
minutes: number;
seconds: number;
};
// Intro playback props
isPlayingIntro?: boolean;
skipAllIntros?: () => void;
intros?: BaseItemDto[];
}
export const BottomControls: FC<BottomControlsProps> = ({
@@ -92,9 +87,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
trickPlayUrl,
trickplayInfo,
time,
isPlayingIntro = false,
skipAllIntros,
intros = [],
}) => {
const { settings } = useSettings();
const insets = useSafeAreaInsets();
@@ -141,14 +133,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
)}
</View>
<View className='flex flex-row space-x-2 shrink-0'>
{/* Skip Intro button - shows when playing intro videos */}
{isPlayingIntro && intros.length > 0 && skipAllIntros && (
<SkipButton
showButton={true}
onPress={skipAllIntros}
buttonText='Skip Intro'
/>
)}
<SkipButton
showButton={showSkipButton}
onPress={skipIntro}

View File

@@ -72,10 +72,6 @@ interface Props {
getTechnicalInfo?: () => Promise<TechnicalInfo>;
playMethod?: "DirectPlay" | "DirectStream" | "Transcode";
transcodeReasons?: string[];
// Intro playback props
isPlayingIntro?: boolean;
skipAllIntros?: () => void;
intros?: BaseItemDto[];
}
export const Controls: FC<Props> = ({
@@ -105,9 +101,6 @@ export const Controls: FC<Props> = ({
getTechnicalInfo,
playMethod,
transcodeReasons,
isPlayingIntro = false,
skipAllIntros,
intros = [],
}) => {
const offline = useOfflineMode();
const { settings, updateSettings } = useSettings();
@@ -561,9 +554,6 @@ export const Controls: FC<Props> = ({
trickPlayUrl={trickPlayUrl}
trickplayInfo={trickplayInfo}
time={isSliding || showRemoteBubble ? time : remoteTime}
isPlayingIntro={isPlayingIntro}
skipAllIntros={skipAllIntros}
intros={intros}
/>
</Animated.View>
</>

View File

@@ -1,46 +0,0 @@
import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useEffect, useState } from "react";
import { getIntros } from "@/utils/intros";
interface UseIntroPlaybackProps {
api: Api | null;
itemId: string | null;
userId?: string;
}
export function useIntroPlayback({
api,
itemId,
userId,
}: UseIntroPlaybackProps) {
const [intros, setIntros] = useState<BaseItemDto[]>([]);
const [isPlayingIntro, setIsPlayingIntro] = useState(false);
useEffect(() => {
async function fetchIntros() {
if (!api || !itemId) return;
const introItems = await getIntros(api, itemId, userId);
setIntros(introItems);
// Set isPlayingIntro to true when intros are available
setIsPlayingIntro(introItems.length > 0);
}
fetchIntros();
}, [api, itemId, userId]);
// Only play the first intro if intros are available.. might be nice to configure this at some point with tags or something 🤷‍♂️
const currentIntro = intros.length > 0 ? intros[0] : null;
const skipAllIntros = () => {
setIsPlayingIntro(false);
};
return {
intros,
currentIntro,
isPlayingIntro,
skipAllIntros,
};
}

View File

@@ -109,7 +109,7 @@
"react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"sonner-native": "0.21.2",
"sonner-native": "0.23.0",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
"zod": "4.1.13"

View File

@@ -1,28 +0,0 @@
import type { Api } from "@jellyfin/sdk";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
/**
* Fetches intro items for a given media item using the Jellyfin SDK
* @param api - The Jellyfin API instance
* @param itemId - The ID of the media item
* @param userId - Optional user ID
* @returns Promise<BaseItemDto[]> - Array of intro items
*/
export async function getIntros(
api: Api,
itemId: string,
userId?: string,
): Promise<BaseItemDto[]> {
try {
const response = await getUserLibraryApi(api).getIntros({
itemId,
userId,
});
return response.data.Items || [];
} catch (error) {
console.error("Error fetching intros:", error);
return [];
}
}