mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-02 03:58:36 +01:00
fix(playback): dispatch each remote command once; stabilise controllers
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { router, Stack } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { GestureDetector } from "react-native-gesture-handler";
|
||||
@@ -189,23 +189,37 @@ export default function CastingPlayerScreen() {
|
||||
loadingEpisodeId != null && loadingEpisodeId !== currentItem?.Id;
|
||||
|
||||
// Expose this player to the app-wide remote-control surface while a cast
|
||||
// session is connected. `castingControls` is the live useCasting result.
|
||||
// session is connected. The individual useCasting methods are each
|
||||
// useCallback-wrapped and stable, so depend on them directly rather than on
|
||||
// the whole `castingControls` object literal (rebuilt every render).
|
||||
const {
|
||||
togglePlayPause: castTogglePlayPause,
|
||||
pause: castPause,
|
||||
play: castPlay,
|
||||
stop: castStop,
|
||||
seek: castSeek,
|
||||
setVolume: castSetVolume,
|
||||
} = castingControls;
|
||||
// toggleMute reads the latest volume without making `volume` a useMemo dep.
|
||||
const volumeRef = useRef(volume);
|
||||
volumeRef.current = volume;
|
||||
|
||||
const castController = useMemo<PlaybackController>(
|
||||
() => ({
|
||||
playPause: () => {
|
||||
castingControls.togglePlayPause();
|
||||
castTogglePlayPause();
|
||||
},
|
||||
pause: () => {
|
||||
castingControls.pause();
|
||||
castPause();
|
||||
},
|
||||
unpause: () => {
|
||||
castingControls.play();
|
||||
castPlay();
|
||||
},
|
||||
stop: () => {
|
||||
castingControls.stop();
|
||||
castStop();
|
||||
},
|
||||
seek: (positionMs) => {
|
||||
castingControls.seek(positionMs);
|
||||
castSeek(positionMs);
|
||||
},
|
||||
next: () => {
|
||||
if (nextEpisode) loadEpisode(nextEpisode);
|
||||
@@ -215,13 +229,24 @@ export default function CastingPlayerScreen() {
|
||||
if (idx > 0) loadEpisode(episodes[idx - 1]);
|
||||
},
|
||||
setVolume: (level) => {
|
||||
castingControls.setVolume(level);
|
||||
castSetVolume(level);
|
||||
},
|
||||
toggleMute: () => {
|
||||
castingControls.setVolume(castingControls.volume > 0 ? 0 : 1);
|
||||
castSetVolume(volumeRef.current > 0 ? 0 : 1);
|
||||
},
|
||||
}),
|
||||
[castingControls, episodes, nextEpisode, loadEpisode, currentItem?.Id],
|
||||
[
|
||||
castTogglePlayPause,
|
||||
castPause,
|
||||
castPlay,
|
||||
castStop,
|
||||
castSeek,
|
||||
castSetVolume,
|
||||
episodes,
|
||||
nextEpisode,
|
||||
loadEpisode,
|
||||
currentItem?.Id,
|
||||
],
|
||||
);
|
||||
|
||||
useRegisterPlaybackController(
|
||||
|
||||
@@ -343,26 +343,6 @@ export default function page() {
|
||||
reportPlaybackStart();
|
||||
}, [stream, api, offline]);
|
||||
|
||||
const togglePlay = async () => {
|
||||
lightHapticFeedback();
|
||||
setIsPlaying(!isPlaying);
|
||||
if (isPlaying) {
|
||||
await videoRef.current?.pause();
|
||||
const progressInfo = currentPlayStateInfo();
|
||||
if (progressInfo) {
|
||||
playbackManager.reportPlaybackProgress(progressInfo);
|
||||
}
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
const progressInfo = currentPlayStateInfo();
|
||||
if (!offline && api) {
|
||||
await getPlaystateApi(api).reportPlaybackStart({
|
||||
playbackStartInfo: progressInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reportPlaybackStopped = useCallback(async () => {
|
||||
if (!item?.Id || !stream?.sessionId || offline || !api) return;
|
||||
|
||||
@@ -431,6 +411,35 @@ export default function page() {
|
||||
isMuted,
|
||||
]);
|
||||
|
||||
// Declared after currentPlayStateInfo so the dependency array can reference
|
||||
// it without hitting the temporal dead zone.
|
||||
const togglePlay = useCallback(async () => {
|
||||
lightHapticFeedback();
|
||||
setIsPlaying(!isPlaying);
|
||||
if (isPlaying) {
|
||||
await videoRef.current?.pause();
|
||||
const progressInfo = currentPlayStateInfo();
|
||||
if (progressInfo) {
|
||||
playbackManager.reportPlaybackProgress(progressInfo);
|
||||
}
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
const progressInfo = currentPlayStateInfo();
|
||||
if (!offline && api) {
|
||||
await getPlaystateApi(api).reportPlaybackStart({
|
||||
playbackStartInfo: progressInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
lightHapticFeedback,
|
||||
isPlaying,
|
||||
currentPlayStateInfo,
|
||||
playbackManager,
|
||||
offline,
|
||||
api,
|
||||
]);
|
||||
|
||||
const lastUrlUpdateTime = useSharedValue(0);
|
||||
const wasJustSeeking = useSharedValue(false);
|
||||
const URL_UPDATE_INTERVAL = 30000; // Update URL every 30 seconds instead of every second
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { toast } from "sonner-native";
|
||||
import { activePlaybackControllerAtom } from "@/utils/playback/playbackController";
|
||||
import {
|
||||
@@ -16,9 +16,11 @@ import {
|
||||
/** Handle one remote-control message (call it whenever a new WS message arrives). */
|
||||
export const useRemoteControl = (lastMessage: RemoteWsMessage | null): void => {
|
||||
const controller = useAtomValue(activePlaybackControllerAtom);
|
||||
const handledRef = useRef<RemoteWsMessage | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastMessage) return;
|
||||
if (!lastMessage || lastMessage === handledRef.current) return;
|
||||
handledRef.current = lastMessage;
|
||||
const action = mapRemoteCommand(lastMessage);
|
||||
if (!action) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user