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: {
1919}) {
2020 const focused = useIsFocused();
2121 const active = useRef(focused && AppState.currentState === "active");
22 const fullscreen = useRef(false);
2223 const [attempt, setAttempt] = useState(0);
2324 // Expo's Android player also reports completed playback as idle.
2425 const [loadState, setLoadState] = useState<"pending" | "complete" | "error">("pending");
......@@ -38,10 +39,12 @@ function LoadedMediaVideo(props: {
3839
3940 useEffect(() => {
4041 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.
4245 const subscription = AppState.addEventListener("change", (state) => {
4346 active.current = focused && !props.paused && state === "active";
44 if (!active.current) player.pause();
47 if (state === "inactive" || (state === "background" && !fullscreen.current)) player.pause();
4548 });
4649 return () => subscription.remove();
4750 }, [focused, player, props.paused]);
......@@ -70,6 +73,12 @@ function LoadedMediaVideo(props: {
7073 nativeControls
7174 contentFit="contain"
7275 fullscreenOptions={{ enable: true }}
76 onFullscreenEnter={() => {
77 fullscreen.current = true;
78 }}
79 onFullscreenExit={() => {
80 fullscreen.current = false;
81 }}
7382 allowsPictureInPicture={false}
7483 />
7584 {loadState === "error" || (loadState === "complete" && status === "error") ? (
apps/web/src/components/media/MediaVideoPlayer.tsx+9-1
......@@ -93,11 +93,19 @@ export function MediaVideoPlayer({
9393 const video = videoRef.current;
9494 if (!video) return;
9595 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();
97101 };
98102 document.addEventListener("visibilitychange", pauseWhenHidden);
103 document.addEventListener("fullscreenchange", pauseWhenHidden);
104 video.addEventListener("webkitendfullscreen", pauseWhenHidden);
99105 return () => {
100106 document.removeEventListener("visibilitychange", pauseWhenHidden);
107 document.removeEventListener("fullscreenchange", pauseWhenHidden);
108 video.removeEventListener("webkitendfullscreen", pauseWhenHidden);
101109 video.pause();
102110 };
103111 }, [src, failed, loadAttempt]);