authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-10 19:50:41-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-10 19:50:41-03:00
loga5ac766596bdf6c7d6852ecda453e2a85a7b08d4
tree21b91d9d1c5941f13ff134b0b730bdba7fe68ba0
parent1a9336bcda98e75814c1985e9274d919e580dbc5
signature Signed by PGP key B5690EEEBB952194

fix(media): preserve playback during fullscreen transitions (#11113)


2 files changed, 20 insertions(+), 3 deletions(-)

apps/mobile/src/components/MediaVideoPlayer.tsx+11-2
...@@ -19,6 +19,7 @@ function LoadedMediaVideo(props: {...@@ -19,6 +19,7 @@ function LoadedMediaVideo(props: {
19}) {19}) {
20 const focused = useIsFocused();20 const focused = useIsFocused();
21 const active = useRef(focused && AppState.currentState === "active");21 const active = useRef(focused && AppState.currentState === "active");
22 const fullscreen = useRef(false);
22 const [attempt, setAttempt] = useState(0);23 const [attempt, setAttempt] = useState(0);
23 // Expo's Android player also reports completed playback as idle.24 // Expo's Android player also reports completed playback as idle.
24 const [loadState, setLoadState] = useState<"pending" | "complete" | "error">("pending");25 const [loadState, setLoadState] = useState<"pending" | "complete" | "error">("pending");
...@@ -38,10 +39,12 @@ function LoadedMediaVideo(props: {...@@ -38,10 +39,12 @@ function LoadedMediaVideo(props: {
3839
39 useEffect(() => {40 useEffect(() => {
40 active.current = focused && !props.paused && AppState.currentState === "active";41 active.current = focused && !props.paused && AppState.currentState === "active";
41 if (!active.current) player.pause();42 if (!focused || props.paused || (!active.current && !fullscreen.current)) player.pause();
43 // Native background handling distinguishes Android's fullscreen activity
44 // from leaving the app; React Native reports both as background.
42 const subscription = AppState.addEventListener("change", (state) => {45 const subscription = AppState.addEventListener("change", (state) => {
43 active.current = focused && !props.paused && state === "active";46 active.current = focused && !props.paused && state === "active";
44 if (!active.current) player.pause();47 if (state === "inactive" || (state === "background" && !fullscreen.current)) player.pause();
45 });48 });
46 return () => subscription.remove();49 return () => subscription.remove();
47 }, [focused, player, props.paused]);50 }, [focused, player, props.paused]);
...@@ -70,6 +73,12 @@ function LoadedMediaVideo(props: {...@@ -70,6 +73,12 @@ function LoadedMediaVideo(props: {
70 nativeControls73 nativeControls
71 contentFit="contain"74 contentFit="contain"
72 fullscreenOptions={{ enable: true }}75 fullscreenOptions={{ enable: true }}
76 onFullscreenEnter={() => {
77 fullscreen.current = true;
78 }}
79 onFullscreenExit={() => {
80 fullscreen.current = false;
81 }}
73 allowsPictureInPicture={false}82 allowsPictureInPicture={false}
74 />83 />
75 {loadState === "error" || (loadState === "complete" && status === "error") ? (84 {loadState === "error" || (loadState === "complete" && status === "error") ? (
apps/web/src/components/media/MediaVideoPlayer.tsx+9-1
...@@ -93,11 +93,19 @@ export function MediaVideoPlayer({...@@ -93,11 +93,19 @@ export function MediaVideoPlayer({
93 const video = videoRef.current;93 const video = videoRef.current;
94 if (!video) return;94 if (!video) return;
95 const pauseWhenHidden = () => {95 const pauseWhenHidden = () => {
96 if (document.hidden) video.pause();96 // Native fullscreen can hide the inline page while this video is still visible.
97 const fullscreen =
98 document.fullscreenElement?.contains(video) ||
99 ("webkitDisplayingFullscreen" in video && video.webkitDisplayingFullscreen === true);
100 if (document.hidden && !fullscreen) video.pause();
97 };101 };
98 document.addEventListener("visibilitychange", pauseWhenHidden);102 document.addEventListener("visibilitychange", pauseWhenHidden);
103 document.addEventListener("fullscreenchange", pauseWhenHidden);
104 video.addEventListener("webkitendfullscreen", pauseWhenHidden);
99 return () => {105 return () => {
100 document.removeEventListener("visibilitychange", pauseWhenHidden);106 document.removeEventListener("visibilitychange", pauseWhenHidden);
107 document.removeEventListener("fullscreenchange", pauseWhenHidden);
108 video.removeEventListener("webkitendfullscreen", pauseWhenHidden);
101 video.pause();109 video.pause();
102 };110 };
103 }, [src, failed, loadAttempt]);111 }, [src, failed, loadAttempt]);