fix: trickplay and re-rendering issues

This commit is contained in:
Fredrik Burmester
2025-08-20 09:59:03 +02:00
parent e7161bc9ab
commit d795e82581
3 changed files with 152 additions and 31 deletions

View File

@@ -15,7 +15,7 @@ import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Alert, Platform, View } from "react-native"; import { Alert, Platform, View } from "react-native";
import { useSharedValue } from "react-native-reanimated"; import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
import { BITRATES } from "@/components/BitrateSelector"; import { BITRATES } from "@/components/BitrateSelector";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
@@ -98,7 +98,7 @@ export default function page() {
/** Playback position in ticks. */ /** Playback position in ticks. */
playbackPosition?: string; playbackPosition?: string;
}>(); }>();
const [settings] = useSettings(); const [_settings] = useSettings();
const offline = offlineStr === "true"; const offline = offlineStr === "true";
const playbackManager = usePlaybackManager(); const playbackManager = usePlaybackManager();
@@ -280,11 +280,15 @@ export default function page() {
]); ]);
const stop = useCallback(() => { const stop = useCallback(() => {
// Update URL with final playback position before stopping
router.setParams({
playbackPosition: msToTicks(progress.get()).toString(),
});
reportPlaybackStopped(); reportPlaybackStopped();
setIsPlaybackStopped(true); setIsPlaybackStopped(true);
videoRef.current?.stop(); videoRef.current?.stop();
revalidateProgressCache(); revalidateProgressCache();
}, [videoRef, reportPlaybackStopped]); }, [videoRef, reportPlaybackStopped, progress]);
useEffect(() => { useEffect(() => {
const beforeRemoveListener = navigation.addListener("beforeRemove", stop); const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
@@ -293,7 +297,7 @@ export default function page() {
}; };
}, [navigation, stop]); }, [navigation, stop]);
const currentPlayStateInfo = () => { const currentPlayStateInfo = useCallback(() => {
if (!stream) return; if (!stream) return;
return { return {
itemId: item?.Id!, itemId: item?.Id!,
@@ -309,7 +313,32 @@ export default function page() {
repeatMode: RepeatMode.RepeatNone, repeatMode: RepeatMode.RepeatNone,
playbackOrder: PlaybackOrder.Default, playbackOrder: PlaybackOrder.Default,
}; };
}; }, [
stream,
item?.Id,
audioIndex,
subtitleIndex,
mediaSourceId,
progress,
isPlaying,
isMuted,
]);
const lastUrlUpdateTime = useSharedValue(0);
const wasJustSeeking = useSharedValue(false);
const URL_UPDATE_INTERVAL = 30000; // Update URL every 30 seconds instead of every second
// Track when seeking ends to update URL immediately
useAnimatedReaction(
() => isSeeking.get(),
(currentSeeking, previousSeeking) => {
if (previousSeeking && !currentSeeking) {
// Seeking just ended
wasJustSeeking.value = true;
}
},
[],
);
const onProgress = useCallback( const onProgress = useCallback(
async (data: ProgressUpdatePayload) => { async (data: ProgressUpdatePayload) => {
@@ -322,10 +351,20 @@ export default function page() {
progress.set(currentTime); progress.set(currentTime);
// Update the playback position in the URL. // Update URL immediately after seeking, or every 30 seconds during normal playback
router.setParams({ const now = Date.now();
playbackPosition: msToTicks(currentTime).toString(), const shouldUpdateUrl = wasJustSeeking.get();
}); wasJustSeeking.value = false;
if (
shouldUpdateUrl ||
now - lastUrlUpdateTime.get() > URL_UPDATE_INTERVAL
) {
router.setParams({
playbackPosition: msToTicks(currentTime).toString(),
});
lastUrlUpdateTime.value = now;
}
if (!item?.Id) return; if (!item?.Id) return;
@@ -398,6 +437,7 @@ export default function page() {
console.error("Error toggling mute:", error); console.error("Error toggling mute:", error);
} }
}, [previousVolume]); }, [previousVolume]);
const volumeDownCb = useCallback(async () => { const volumeDownCb = useCallback(async () => {
if (Platform.isTV) return; if (Platform.isTV) return;
@@ -512,7 +552,7 @@ export default function page() {
/** Whether the stream we're playing is not transcoding*/ /** Whether the stream we're playing is not transcoding*/
const notTranscoding = !stream?.mediaSource.TranscodingUrl; const notTranscoding = !stream?.mediaSource.TranscodingUrl;
/** The initial options to pass to the VLC Player */ /** The initial options to pass to the VLC Player */
const initOptions = [`--sub-text-scale=${settings.subtitleSize}`]; const initOptions = [``];
if ( if (
chosenSubtitleTrack && chosenSubtitleTrack &&
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream) (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
@@ -537,6 +577,54 @@ export default function page() {
return () => setIsMounted(false); return () => setIsMounted(false);
}, []); }, []);
// Memoize video ref functions to prevent unnecessary re-renders
const startPictureInPicture = useMemo(
() => videoRef.current?.startPictureInPicture,
[isVideoLoaded],
);
const play = useMemo(
() => videoRef.current?.play || (() => {}),
[isVideoLoaded],
);
const pause = useMemo(
() => videoRef.current?.pause || (() => {}),
[isVideoLoaded],
);
const seek = useMemo(
() => videoRef.current?.seekTo || (() => {}),
[isVideoLoaded],
);
const getAudioTracks = useMemo(
() => videoRef.current?.getAudioTracks,
[isVideoLoaded],
);
const getSubtitleTracks = useMemo(
() => videoRef.current?.getSubtitleTracks,
[isVideoLoaded],
);
const setSubtitleTrack = useMemo(
() => videoRef.current?.setSubtitleTrack,
[isVideoLoaded],
);
const setSubtitleURL = useMemo(
() => videoRef.current?.setSubtitleURL,
[isVideoLoaded],
);
const setAudioTrack = useMemo(
() => videoRef.current?.setAudioTrack,
[isVideoLoaded],
);
const setVideoAspectRatio = useMemo(
() => videoRef.current?.setVideoAspectRatio,
[isVideoLoaded],
);
const setVideoScaleFactor = useMemo(
() => videoRef.current?.setVideoScaleFactor,
[isVideoLoaded],
);
console.log("Debug: component render"); // Uncomment to debug re-renders
// Show error UI first, before checking loading/missingdata // Show error UI first, before checking loading/missingdata
if (itemStatus.isError || streamStatus.isError) { if (itemStatus.isError || streamStatus.isError) {
return ( return (
@@ -567,7 +655,7 @@ export default function page() {
<View <View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: "blue", backgroundColor: "black",
height: "100%", height: "100%",
width: "100%", width: "100%",
}} }}
@@ -624,19 +712,19 @@ export default function page() {
showControls={showControls} showControls={showControls}
setShowControls={setShowControls} setShowControls={setShowControls}
isVideoLoaded={isVideoLoaded} isVideoLoaded={isVideoLoaded}
startPictureInPicture={videoRef.current?.startPictureInPicture} startPictureInPicture={startPictureInPicture}
play={videoRef.current?.play || (() => {})} play={play}
pause={videoRef.current?.pause || (() => {})} pause={pause}
seek={videoRef.current?.seekTo || (() => {})} seek={seek}
enableTrickplay={true} enableTrickplay={true}
getAudioTracks={videoRef.current?.getAudioTracks} getAudioTracks={getAudioTracks}
getSubtitleTracks={videoRef.current?.getSubtitleTracks} getSubtitleTracks={getSubtitleTracks}
offline={offline} offline={offline}
setSubtitleTrack={videoRef.current?.setSubtitleTrack} setSubtitleTrack={setSubtitleTrack}
setSubtitleURL={videoRef.current?.setSubtitleURL} setSubtitleURL={setSubtitleURL}
setAudioTrack={videoRef.current?.setAudioTrack} setAudioTrack={setAudioTrack}
setVideoAspectRatio={videoRef.current?.setVideoAspectRatio} setVideoAspectRatio={setVideoAspectRatio}
setVideoScaleFactor={videoRef.current?.setVideoScaleFactor} setVideoScaleFactor={setVideoScaleFactor}
aspectRatio={aspectRatio} aspectRatio={aspectRatio}
scaleFactor={scaleFactor} scaleFactor={scaleFactor}
setAspectRatio={setAspectRatio} setAspectRatio={setAspectRatio}

View File

@@ -282,18 +282,31 @@ export const Controls: FC<Props> = ({
const effectiveProgress = useSharedValue(0); const effectiveProgress = useSharedValue(0);
// Recompute progress whenever remote scrubbing is active // Recompute progress whenever remote scrubbing is active or when progress significantly changes
useAnimatedReaction( useAnimatedReaction(
() => ({ () => ({
isScrubbing: isRemoteScrubbing.value, isScrubbing: isRemoteScrubbing.value,
scrub: remoteScrubProgress.value, scrub: remoteScrubProgress.value,
actual: progress.value, actual: progress.value,
}), }),
(current) => { (current, previous) => {
effectiveProgress.value = // Always update if scrubbing state changed or we're currently scrubbing
current.isScrubbing && current.scrub != null if (
? current.scrub current.isScrubbing !== previous?.isScrubbing ||
: current.actual; current.isScrubbing
) {
effectiveProgress.value =
current.isScrubbing && current.scrub != null
? current.scrub
: current.actual;
} else {
// When not scrubbing, only update if progress changed significantly (1 second)
const progressUnit = isVlc ? 1000 : 10000000; // 1 second in ms or ticks
const progressDiff = Math.abs(current.actual - effectiveProgress.value);
if (progressDiff >= progressUnit) {
effectiveProgress.value = current.actual;
}
}
}, },
[], [],
); );
@@ -450,6 +463,9 @@ export const Controls: FC<Props> = ({
[goToNextItem], [goToNextItem],
); );
const lastCurrentTimeRef = useRef(0);
const lastRemainingTimeRef = useRef(0);
const updateTimes = useCallback( const updateTimes = useCallback(
(currentProgress: number, maxValue: number) => { (currentProgress: number, maxValue: number) => {
const current = isVlc ? currentProgress : ticksToSeconds(currentProgress); const current = isVlc ? currentProgress : ticksToSeconds(currentProgress);
@@ -457,8 +473,25 @@ export const Controls: FC<Props> = ({
? maxValue - currentProgress ? maxValue - currentProgress
: ticksToSeconds(maxValue - currentProgress); : ticksToSeconds(maxValue - currentProgress);
setCurrentTime(current); // Only update state if the displayed time actually changed (avoid sub-second updates)
setRemainingTime(remaining); const currentSeconds = Math.floor(current / (isVlc ? 1000 : 1));
const remainingSeconds = Math.floor(remaining / (isVlc ? 1000 : 1));
const lastCurrentSeconds = Math.floor(
lastCurrentTimeRef.current / (isVlc ? 1000 : 1),
);
const lastRemainingSeconds = Math.floor(
lastRemainingTimeRef.current / (isVlc ? 1000 : 1),
);
if (
currentSeconds !== lastCurrentSeconds ||
remainingSeconds !== lastRemainingSeconds
) {
setCurrentTime(current);
setRemainingTime(remaining);
lastCurrentTimeRef.current = current;
lastRemainingTimeRef.current = remaining;
}
}, },
[goToNextItem, isVlc], [goToNextItem, isVlc],
); );

View File

@@ -70,7 +70,7 @@ export const usePlaybackManager = ({
useDownload(); useDownload();
/** Whether the device is online. actually it's connected to the internet. */ /** Whether the device is online. actually it's connected to the internet. */
const isOnline = netInfo.isConnected; const isOnline = useMemo(() => netInfo.isConnected, [netInfo.isConnected]);
// Adjacent episodes logic // Adjacent episodes logic
const { data: adjacentItems } = useQuery({ const { data: adjacentItems } = useQuery({