forked from Ninjalama/streamyfin_mirror
fix: trickplay and re-rendering issues
This commit is contained in:
@@ -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/missing‐data
|
// Show error UI first, before checking loading/missing‐data
|
||||||
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}
|
||||||
|
|||||||
@@ -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],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user