From d9046f76a280049e698d941f10e0b0312d4158f6 Mon Sep 17 00:00:00 2001 From: "Jules (via MelvinBot)" Date: Wed, 4 Mar 2026 22:35:38 +0000 Subject: [PATCH 1/3] Delay video offline state to prevent flickering during intermittent network During intermittent network conditions, the isOffline flag from useNetwork() flips rapidly, causing the video loading spinner to disappear and the offline indicator to flash. This also triggers replaceAsync('') which destructively clears the video source mid-stream. Introduce a local isVideoOffline state in BaseVideoPlayer that only becomes true after isOffline has been continuously true for 10 seconds. This keeps the loading spinner visible during brief offline blips while still showing the offline indicator for confirmed sustained offline periods. Co-authored-by: Jules --- src/CONST/index.ts | 1 + .../VideoPlayer/BaseVideoPlayer.tsx | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3f35c2c1e595b..98848d1dc34ec 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6209,6 +6209,7 @@ const CONST = { NORMAL: 8, }, DEFAULT_VIDEO_DIMENSIONS: {width: 1900, height: 1400}, + OFFLINE_THRESHOLD: 10000, }, INTRO_CHOICES: { diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 51574db17dd2e..6f02443ffa31d 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -59,6 +59,7 @@ function BaseVideoPlayer({ const {isFullScreenRef} = useFullScreenState(); const isOffline = useNetwork().isOffline; + const [isVideoOffline, setIsVideoOffline] = useState(false); const session = useSession(); const encryptedAuthToken = session?.encryptedAuthToken ?? ''; const [duration, setDuration] = useState(videoDuration); @@ -109,11 +110,24 @@ function BaseVideoPlayer({ }); useEffect(() => { - if (!(isOffline && isLoading)) { + if (!isOffline) { + setIsVideoOffline(false); + return; + } + + const timer = setTimeout(() => { + setIsVideoOffline(true); + }, CONST.VIDEO_PLAYER.OFFLINE_THRESHOLD); + + return () => clearTimeout(timer); + }, [isOffline]); + + useEffect(() => { + if (!(isVideoOffline && isLoading)) { return; } videoPlayerRef.current.replaceAsync(''); - }, [isLoading, isOffline]); + }, [isLoading, isVideoOffline]); const videoViewRef = useRef(null); const videoPlayerElementParentRef = useRef(null); @@ -124,18 +138,18 @@ function BaseVideoPlayer({ const isCurrentlyURLSet = currentlyPlayingURL === url; const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); const shouldShowErrorIndicator = useMemo(() => { - // No need to set hasError while offline, since the offline indicator is already shown. + // No need to set hasError while confirmed offline, since the offline indicator is already shown. // Once the user reconnects, if the video is unsupported, the error will be triggered again. - return hasError && !isOffline; - }, [hasError, isOffline]); + return hasError && !isVideoOffline; + }, [hasError, isVideoOffline]); const shouldShowLoadingIndicator = useMemo(() => { // We want to show LoadingIndicator when video's loading and paused, except when it's loading - // for the first time, then playing/loading may vary. Video should be online and without errors. - return isLoading && (!isPlaying || currentTime <= 0) && !isOffline && !hasError; - }, [currentTime, hasError, isLoading, isOffline, isPlaying]); + // for the first time, then playing/loading may vary. Video should not be confirmed offline and without errors. + return isLoading && (!isPlaying || currentTime <= 0) && !isVideoOffline && !hasError; + }, [currentTime, hasError, isLoading, isVideoOffline, isPlaying]); const shouldShowOfflineIndicator = useMemo(() => { - return isOffline && currentTime + bufferedPosition <= 0; - }, [bufferedPosition, currentTime, isOffline]); + return isVideoOffline && currentTime + bufferedPosition <= 0; + }, [bufferedPosition, currentTime, isVideoOffline]); const {updateVolume} = useVolumeActions(); const {lastNonZeroVolume} = useVolumeState(); useHandleNativeVideoControls({ From a46ff504608791cac0c02130134b5f495afb42c1 Mon Sep 17 00:00:00 2001 From: "Roji Philip (via MelvinBot)" Date: Thu, 5 Mar 2026 01:06:14 +0000 Subject: [PATCH 2/3] Reduce OFFLINE_THRESHOLD from 10s to 7s The threshold for intermittent networks via 'Simulate poor internet connection' is 5 seconds, so 7 seconds is a more appropriate threshold to distinguish brief connectivity drops from sustained offline state. Co-authored-by: Roji Philip --- src/CONST/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 98848d1dc34ec..9913ae01eb94f 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6209,7 +6209,7 @@ const CONST = { NORMAL: 8, }, DEFAULT_VIDEO_DIMENSIONS: {width: 1900, height: 1400}, - OFFLINE_THRESHOLD: 10000, + OFFLINE_THRESHOLD: 7000, }, INTRO_CHOICES: { From 7ad96f3015a4132627e57858b9b8cc686a99dc13 Mon Sep 17 00:00:00 2001 From: "Roji Philip (via MelvinBot)" Date: Fri, 6 Mar 2026 19:09:43 +0000 Subject: [PATCH 3/3] Guard replaceAsync('') with isOffline to prevent clearing source after reconnection Adds isOffline as an extra condition to the effect that calls replaceAsync('') so it never fires when the network has already reconnected, closing a theoretical race window where isVideoOffline could still be true for one render cycle after isOffline flips to false. Co-authored-by: Roji Philip --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 6f02443ffa31d..01a169c211eab 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -123,11 +123,11 @@ function BaseVideoPlayer({ }, [isOffline]); useEffect(() => { - if (!(isVideoOffline && isLoading)) { + if (!(isVideoOffline && isLoading && isOffline)) { return; } videoPlayerRef.current.replaceAsync(''); - }, [isLoading, isVideoOffline]); + }, [isLoading, isVideoOffline, isOffline]); const videoViewRef = useRef(null); const videoPlayerElementParentRef = useRef(null);