Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix video sharing #38407

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b307865
Add window dimensions blocking POC
Skalakid Mar 15, 2024
344c68d
Enhance bug fix code structure
Skalakid Mar 15, 2024
deec3ab
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 18, 2024
4188a33
Fix TS errors
Skalakid Mar 18, 2024
29a802a
Fix video sharing bug when changing window dimensions while video is …
Skalakid Mar 18, 2024
4bbae6c
Restore code that is used in other places
Skalakid Mar 18, 2024
cab0496
Fix video starting playing when exiting fullscreen with paused video
Skalakid Mar 18, 2024
f54eb12
Change fix structure
Skalakid Mar 18, 2024
05ed715
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 18, 2024
1f04efc
Add locked screen dimensions updating when orientation changes
Skalakid Mar 19, 2024
82ca579
Enhance device orientation checking
Skalakid Mar 19, 2024
59ff4a2
Fix video pausing when dismissing fullscreen mode
Skalakid Mar 19, 2024
4e06538
Change orginal size detction
Skalakid Mar 19, 2024
1ca5515
Add comment
Skalakid Mar 19, 2024
72b0fb1
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 20, 2024
df3db7d
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 20, 2024
ac35413
Add review changes
Skalakid Mar 20, 2024
3f75fe3
Fix fullscreen mode bug
Skalakid Mar 20, 2024
5b35c6e
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 21, 2024
5622588
Fix fullscreen context types
Skalakid Mar 21, 2024
328fbd8
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 22, 2024
e75f1c4
Add review changes
Skalakid Mar 22, 2024
75896d2
Fix video pausing in fullscreen mode
Skalakid Mar 25, 2024
9facbb7
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 26, 2024
47f92d3
Merge branch 'main' into @Skalakid/fix-video-element-sharing
Skalakid Mar 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvide
import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider';
import ThemeProvider from './components/ThemeProvider';
import ThemeStylesProvider from './components/ThemeStylesProvider';
import {FullScreenContextProvider} from './components/VideoPlayerContexts/FullScreenContext';
import {PlaybackContextProvider} from './components/VideoPlayerContexts/PlaybackContext';
import {VideoPopoverMenuContextProvider} from './components/VideoPlayerContexts/VideoPopoverMenuContext';
import {VolumeContextProvider} from './components/VideoPlayerContexts/VolumeContext';
Expand Down Expand Up @@ -78,6 +79,7 @@ function App({url}: AppProps) {
ActiveElementRoleProvider,
ActiveWorkspaceContextProvider,
PlaybackContextProvider,
FullScreenContextProvider,
VolumeContextProvider,
VideoPopoverMenuContextProvider,
]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr
isHovered={isModalHovered}
isFocused={isFocused}
duration={item.duration}
isUsedInCarousel
/>
</View>

Expand Down
21 changes: 18 additions & 3 deletions src/components/Attachments/AttachmentCarousel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import * as Illustrations from '@components/Icon/Illustrations';
import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -32,6 +33,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
const theme = useTheme();
const {translate} = useLocalize();
const styles = useThemeStyles();
const {isFullScreenRef} = useFullScreenContext();
const scrollRef = useRef<FlatList>(null);

const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
Expand Down Expand Up @@ -76,6 +78,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
/** Updates the page state when the user navigates between attachments */
const updatePage = useCallback(
({viewableItems}: UpdatePageProps) => {
if (isFullScreenRef.current) {
return;
}

Keyboard.dismiss();

// Since we can have only one item in view at a time, we can use the first item in the array
Expand All @@ -95,12 +101,16 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
onNavigate(entry.item);
}
},
[onNavigate],
[isFullScreenRef, onNavigate],
);

/** Increments or decrements the index to get another selected item */
const cycleThroughAttachments = useCallback(
(deltaSlide: number) => {
if (isFullScreenRef.current) {
return;
}

const nextIndex = page + deltaSlide;
const nextItem = attachments[nextIndex];

Expand All @@ -110,7 +120,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,

scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen});
},
[attachments, canUseTouchScreen, page],
[attachments, canUseTouchScreen, isFullScreenRef, page],
);

const extractItemKey = useCallback(
Expand Down Expand Up @@ -145,7 +155,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
return (
<View
style={[styles.flex1, styles.attachmentCarouselContainer]}
onLayout={({nativeEvent}) => setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width))}
onLayout={({nativeEvent}) => {
if (isFullScreenRef.current) {
return;
}
setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width));
}}
onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ type AttachmentViewVideoProps = Pick<AttachmentViewProps, 'duration' | 'isHovere
};

function AttachmentViewVideo({source, isHovered = false, shouldUseSharedVideoElement = false, duration = 0}: AttachmentViewVideoProps) {
const {isSmallScreen} = useWindowDimensions();
const {isSmallScreenWidth} = useWindowDimensions();
const styles = useThemeStyles();

return (
<VideoPlayer
url={source}
shouldUseSharedVideoElement={shouldUseSharedVideoElement && !isSmallScreen}
shouldUseSharedVideoElement={shouldUseSharedVideoElement && !isSmallScreenWidth}
isVideoHovered={isHovered}
videoDuration={duration}
style={[styles.w100, styles.h100]}
Expand Down
94 changes: 47 additions & 47 deletions src/components/VideoPlayer/BaseVideoPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@ import _ from 'underscore';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import Hoverable from '@components/Hoverable';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import VideoPopoverMenu from '@components/VideoPopoverMenu';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import * as Browser from '@libs/Browser';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import CONST from '@src/CONST';
import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes';
import shouldReplayVideo from './shouldReplayVideo';
import * as VideoUtils from './utils';
import VideoPlayerControls from './VideoPlayerControls';

const isMobileSafari = Browser.isMobileSafari();

function BaseVideoPlayer({
url,
resizeMode,
Expand All @@ -44,8 +42,19 @@ function BaseVideoPlayer({
isVideoHovered,
}) {
const styles = useThemeStyles();
const {pauseVideo, playVideo, currentlyPlayingURL, updateSharedElements, sharedElement, originalParent, shareVideoPlayerElements, currentVideoPlayerRef, updateCurrentlyPlayingURL} =
usePlaybackContext();
const {
pauseVideo,
playVideo,
currentlyPlayingURL,
updateSharedElements,
sharedElement,
originalParent,
shareVideoPlayerElements,
currentVideoPlayerRef,
updateCurrentlyPlayingURL,
videoResumeTryNumber,
} = usePlaybackContext();
const {isFullScreenRef} = useFullScreenContext();
const {isOffline} = useNetwork();
const [duration, setDuration] = useState(videoDuration * 1000);
const [position, setPosition] = useState(0);
Expand All @@ -60,24 +69,21 @@ function BaseVideoPlayer({
const videoPlayerElementParentRef = useRef(null);
const videoPlayerElementRef = useRef(null);
const sharedVideoPlayerParentRef = useRef(null);
const videoResumeTryNumber = useRef(0);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
const isCurrentlyURLSet = currentlyPlayingURL === url;
const isUploading = _.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => url.startsWith(prefix));
const shouldUseSharedVideoElementRef = useRef(shouldUseSharedVideoElement);

const [isFullscreen, setIsFullscreen] = useState(false);
const videoStateRef = useRef(null);

const togglePlayCurrentVideo = useCallback(() => {
videoResumeTryNumber.current = 0;
if (!isCurrentlyURLSet) {
updateCurrentlyPlayingURL(url);
} else if (isPlaying && !isFullscreen) {
} else if (isPlaying) {
pauseVideo();
} else if (!isFullscreen) {
} else {
playVideo();
}
}, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, isFullscreen]);
}, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]);

const showPopoverMenu = (e) => {
setPopoverAnchorPosition({horizontal: e.nativeEvent.pageX, vertical: e.nativeEvent.pageY});
Expand All @@ -99,7 +105,7 @@ function BaseVideoPlayer({
}
videoResumeTryNumber.current -= 1;
},
[playVideo],
[playVideo, videoResumeTryNumber],
);

const handlePlaybackStatusUpdate = useCallback(
Expand All @@ -118,7 +124,7 @@ function BaseVideoPlayer({
setIsBuffering(e.isBuffering || false);
setDuration(currentDuration);
setPosition(currentPositon);

videoStateRef.current = e;
onPlaybackStatusUpdate(e);
},
[onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration],
Expand All @@ -127,22 +133,20 @@ function BaseVideoPlayer({
const handleFullscreenUpdate = useCallback(
(e) => {
onFullscreenUpdate(e);

setIsFullscreen(e.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_PRESENT);

// fix for iOS native and mWeb: when switching to fullscreen and then exiting
// the fullscreen mode while playing, the video pauses
if (!isPlaying || e.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
return;
if (e.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
isFullScreenRef.current = false;
// we need to use video state ref to check if video is playing, to catch proper state after exiting fullscreen
// and also fix a bug with fullscreen mode dismissing when handleFullscreenUpdate function changes
if (videoStateRef.current && videoStateRef.current.isPlaying) {
pauseVideo();
playVideo();
videoResumeTryNumber.current = 3;
}
}

if (isMobileSafari) {
pauseVideo();
}
playVideo();
videoResumeTryNumber.current = 3;
},
[isPlaying, onFullscreenUpdate, pauseVideo, playVideo],
[isFullScreenRef, onFullscreenUpdate, pauseVideo, playVideo, videoResumeTryNumber],
);

const bindFunctions = useCallback(() => {
Expand All @@ -156,45 +160,37 @@ function BaseVideoPlayer({
}, [currentVideoPlayerRef, handleFullscreenUpdate, handlePlaybackStatusUpdate]);

useEffect(() => {
if (!isUploading) {
if (!isUploading || !videoPlayerRef.current) {
return;
}

// If we are uploading a new video, we want to immediately set the video player ref.
currentVideoPlayerRef.current = videoPlayerRef.current;
}, [url, currentVideoPlayerRef, isUploading]);

useEffect(() => {
shouldUseSharedVideoElementRef.current = shouldUseSharedVideoElement;
}, [shouldUseSharedVideoElement]);

useEffect(
() => () => {
if (shouldUseSharedVideoElementRef.current) {
return;
}

// If it's not a shared video player, clear the video player ref.
currentVideoPlayerRef.current = null;
},
[currentVideoPlayerRef],
);

// update shared video elements
useEffect(() => {
if (shouldUseSharedVideoElement || url !== currentlyPlayingURL) {
if (shouldUseSharedVideoElement || url !== currentlyPlayingURL || isFullScreenRef.current) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Skalakid I wonder why do we need to add isFullScreenRef.current?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isFullScreenRef is responsible for locking the video player when screen orientation changes in full-screen mode. It prevents video players from changing their state because of device rotation or window size changes. Changing state causes the full screen to dismiss. We need it here to block updating currently shared video element

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me reproduce the bug if we remove isFullScreenRef.current? Thanks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, @Skalakid
I'm also curious about test steps that cause unexpected errors when we remove isFullScreenRef.current.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to remove isFullScreenRef.current only in this useEffect if statement.

Copy link
Contributor

@jacobnguyen0000 jacobnguyen0000 May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Skalakid
Thank you for your confirm. I can't reproduce on my end.
Could you please let me know if this issue is reproducible on native devices?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed again, it's not reproducible on my end.

Copy link
Contributor Author

@Skalakid Skalakid May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to remove isFullScreenRef.current only in this useEffect if statement.

@tienifr oh thanks for information, in the video above I deleted isFullScreenRef in whole BaseVideo Player file 😅 So if it works after deleting isFullScreenRef.current in useEffect, just do the test steps from this issue description and if everything works you can remove it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Skalakid
thank you for your check.

return;
}
shareVideoPlayerElements(videoPlayerRef.current, videoPlayerElementParentRef.current, videoPlayerElementRef.current, isUploading);
}, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, updateSharedElements, url, isUploading]);
}, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, updateSharedElements, url, isUploading, isFullScreenRef]);

// append shared video element to new parent (used for example in attachment modal)
useEffect(() => {
if (url !== currentlyPlayingURL || !sharedElement || !shouldUseSharedVideoElement) {
if (url !== currentlyPlayingURL || !sharedElement || isFullScreenRef.current) {
return;
}

const newParentRef = sharedVideoPlayerParentRef.current;

if (!shouldUseSharedVideoElement) {
if (newParentRef && newParentRef.childNodes[0] && newParentRef.childNodes[0].remove) {
newParentRef.childNodes[0].remove();
}
return;
}

videoPlayerRef.current = currentVideoPlayerRef.current;
if (currentlyPlayingURL === url) {
newParentRef.appendChild(sharedElement);
Expand All @@ -204,9 +200,10 @@ function BaseVideoPlayer({
if (!originalParent && !newParentRef.childNodes[0]) {
return;
}
newParentRef.childNodes[0].remove();
originalParent.appendChild(sharedElement);
};
}, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, originalParent, sharedElement, shouldUseSharedVideoElement, url]);
}, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, isFullScreenRef, originalParent, sharedElement, shouldUseSharedVideoElement, url]);

return (
<>
Expand All @@ -222,6 +219,9 @@ function BaseVideoPlayer({
<PressableWithoutFeedback
accessibilityRole="button"
onPress={() => {
if (isFullScreenRef.current) {
return;
}
togglePlayCurrentVideo();
}}
style={styles.flex1}
Expand Down
5 changes: 4 additions & 1 deletion src/components/VideoPlayer/VideoPlayerControls/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
import IconButton from '@components/VideoPlayer/IconButton';
import {convertMillisecondsToTime} from '@components/VideoPlayer/utils';
import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -48,6 +49,7 @@ function VideoPlayerControls({duration, position, url, videoPlayerRef, isPlaying
const styles = useThemeStyles();
const {translate} = useLocalize();
const {updateCurrentlyPlayingURL} = usePlaybackContext();
const {isFullScreenRef} = useFullScreenContext();
const [shouldShowTime, setShouldShowTime] = useState(false);
const iconSpacing = small ? styles.mr3 : styles.mr4;

Expand All @@ -56,9 +58,10 @@ function VideoPlayerControls({duration, position, url, videoPlayerRef, isPlaying
};

const enterFullScreenMode = useCallback(() => {
isFullScreenRef.current = true;
updateCurrentlyPlayingURL(url);
videoPlayerRef.current.presentFullscreenPlayer();
}, [updateCurrentlyPlayingURL, url, videoPlayerRef]);
}, [isFullScreenRef, updateCurrentlyPlayingURL, url, videoPlayerRef]);

const seekPosition = useCallback(
(newPosition: number) => {
Expand Down
34 changes: 34 additions & 0 deletions src/components/VideoPlayerContexts/FullScreenContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, {useCallback, useContext, useMemo, useRef} from 'react';
import type WindowDimensions from '@hooks/useWindowDimensions/types';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {FullScreenContext} from './types';

const Context = React.createContext<FullScreenContext | null>(null);

function FullScreenContextProvider({children}: ChildrenProps) {
const isFullScreenRef = useRef(false);
const lockedWindowDimensionsRef = useRef<WindowDimensions | null>(null);

const lockWindowDimensions = useCallback((newWindowDimensions: WindowDimensions) => {
lockedWindowDimensionsRef.current = newWindowDimensions;
}, []);

const unlockWindowDimensions = useCallback(() => {
lockedWindowDimensionsRef.current = null;
}, []);

const contextValue = useMemo(() => ({isFullScreenRef, lockedWindowDimensionsRef, lockWindowDimensions, unlockWindowDimensions}), [lockWindowDimensions, unlockWindowDimensions]);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

function useFullScreenContext() {
const fullscreenContext = useContext(Context);
if (!fullscreenContext) {
throw new Error('useFullScreenContext must be used within a FullScreenContextProvider');
}
return fullscreenContext;
}

FullScreenContextProvider.displayName = 'FullScreenContextProvider';

export {Context as FullScreenContext, FullScreenContextProvider, useFullScreenContext};
Skalakid marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading