Skip to content

Commit

Permalink
Merge pull request Expensify#38407 from software-mansion-labs/@Skalak…
Browse files Browse the repository at this point in the history
…id/fix-video-element-sharing

Fix video sharing
  • Loading branch information
lakchote authored Mar 28, 2024
2 parents e14ac08 + 47f92d3 commit 5888c68
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 57 deletions.
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) {
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};
Loading

0 comments on commit 5888c68

Please sign in to comment.