Media Viewer: Introduce Picture-in-Picture (#2015)
This commit is contained in:
parent
103e5ed8e9
commit
de772bfd59
Binary file not shown.
Binary file not shown.
@ -37,6 +37,7 @@ import { renderMessageText } from '../common/helpers/renderMessageText';
|
|||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
|
import { exitPictureInPictureIfNeeded } from '../../hooks/usePictureInPicture';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import { useMediaProps } from './hooks/useMediaProps';
|
import { useMediaProps } from './hooks/useMediaProps';
|
||||||
@ -62,6 +63,7 @@ type StateProps = {
|
|||||||
message?: ApiMessage;
|
message?: ApiMessage;
|
||||||
chatMessages?: Record<number, ApiMessage>;
|
chatMessages?: Record<number, ApiMessage>;
|
||||||
collectionIds?: number[];
|
collectionIds?: number[];
|
||||||
|
isHidden?: boolean;
|
||||||
animationLevel: AnimationLevel;
|
animationLevel: AnimationLevel;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
};
|
};
|
||||||
@ -80,6 +82,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
chatMessages,
|
chatMessages,
|
||||||
collectionIds,
|
collectionIds,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
|
isHidden,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -120,6 +123,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const canReport = !!avatarPhoto && !isChatWithSelf;
|
const canReport = !!avatarPhoto && !isChatWithSelf;
|
||||||
|
const isVisible = !isHidden && isOpen;
|
||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
const singleMediaId = webPagePhoto || webPageVideo ? mediaId : undefined;
|
const singleMediaId = webPagePhoto || webPageVideo ? mediaId : undefined;
|
||||||
@ -140,6 +144,12 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
animationKey.current = selectedMediaIndex;
|
animationKey.current = selectedMediaIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisible) {
|
||||||
|
exitPictureInPictureIfNeeded();
|
||||||
|
}
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!IS_SINGLE_COLUMN_LAYOUT) return;
|
if (!IS_SINGLE_COLUMN_LAYOUT) return;
|
||||||
document.body.classList.toggle('is-media-viewer-open', isOpen);
|
document.body.classList.toggle('is-media-viewer-open', isOpen);
|
||||||
@ -164,14 +174,17 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
}, [forceUpdate]);
|
}, [forceUpdate]);
|
||||||
|
|
||||||
const prevMessage = usePrevious<ApiMessage | undefined>(message);
|
const prevMessage = usePrevious<ApiMessage | undefined>(message);
|
||||||
|
const prevIsHidden = usePrevious<boolean | undefined>(isHidden);
|
||||||
const prevOrigin = usePrevious(origin);
|
const prevOrigin = usePrevious(origin);
|
||||||
|
const prevMediaId = usePrevious(mediaId);
|
||||||
const prevAvatarOwner = usePrevious<ApiChat | ApiUser | undefined>(avatarOwner);
|
const prevAvatarOwner = usePrevious<ApiChat | ApiUser | undefined>(avatarOwner);
|
||||||
const prevBestImageData = usePrevious(bestImageData);
|
const prevBestImageData = usePrevious(bestImageData);
|
||||||
const textParts = message ? renderMessageText(message) : undefined;
|
const textParts = message ? renderMessageText(message) : undefined;
|
||||||
const hasFooter = Boolean(textParts);
|
const hasFooter = Boolean(textParts);
|
||||||
|
const shouldAnimateOpening = prevIsHidden && prevMediaId !== mediaId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isGhostAnimation && isOpen && !prevMessage && !prevAvatarOwner) {
|
if (isGhostAnimation && isOpen && (!prevMessage || shouldAnimateOpening) && !prevAvatarOwner) {
|
||||||
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||||
animateOpening(hasFooter, origin!, bestImageData!, dimensions, isVideo, message);
|
animateOpening(hasFooter, origin!, bestImageData!, dimensions, isVideo, message);
|
||||||
}
|
}
|
||||||
@ -181,7 +194,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
animateClosing(prevOrigin!, prevBestImageData!, prevMessage || undefined);
|
animateClosing(prevOrigin!, prevBestImageData!, prevMessage || undefined);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isGhostAnimation, isOpen, origin, prevOrigin, message, prevMessage, prevAvatarOwner,
|
isGhostAnimation, isOpen, shouldAnimateOpening, origin, prevOrigin, message, prevMessage, prevAvatarOwner,
|
||||||
bestImageData, prevBestImageData, dimensions, isVideo, hasFooter,
|
bestImageData, prevBestImageData, dimensions, isVideo, hasFooter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -269,7 +282,12 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowTransition id="MediaViewer" isOpen={isOpen} noCloseTransition={shouldSkipHistoryAnimations}>
|
<ShowTransition
|
||||||
|
id="MediaViewer"
|
||||||
|
isOpen={isOpen}
|
||||||
|
isHidden={isHidden}
|
||||||
|
noCloseTransition={shouldSkipHistoryAnimations}
|
||||||
|
>
|
||||||
<div className="media-viewer-head" dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="media-viewer-head" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{IS_SINGLE_COLUMN_LAYOUT && (
|
{IS_SINGLE_COLUMN_LAYOUT && (
|
||||||
<Button
|
<Button
|
||||||
@ -323,6 +341,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
animationLevel={animationLevel}
|
animationLevel={animationLevel}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
selectMedia={selectMedia}
|
selectMedia={selectMedia}
|
||||||
|
isHidden={isHidden}
|
||||||
onFooterClick={handleFooterClick}
|
onFooterClick={handleFooterClick}
|
||||||
/>
|
/>
|
||||||
</ShowTransition>
|
</ShowTransition>
|
||||||
@ -337,6 +356,7 @@ export default memo(withGlobal(
|
|||||||
mediaId,
|
mediaId,
|
||||||
avatarOwnerId,
|
avatarOwnerId,
|
||||||
origin,
|
origin,
|
||||||
|
isHidden,
|
||||||
} = global.mediaViewer;
|
} = global.mediaViewer;
|
||||||
const {
|
const {
|
||||||
animationLevel,
|
animationLevel,
|
||||||
@ -364,6 +384,7 @@ export default memo(withGlobal(
|
|||||||
origin,
|
origin,
|
||||||
message,
|
message,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
|
isHidden,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -380,6 +401,7 @@ export default memo(withGlobal(
|
|||||||
animationLevel,
|
animationLevel,
|
||||||
origin,
|
origin,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
|
isHidden,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,6 +448,7 @@ export default memo(withGlobal(
|
|||||||
chatMessages,
|
chatMessages,
|
||||||
collectionIds,
|
collectionIds,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
|
isHidden,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -49,6 +49,7 @@ type StateProps = {
|
|||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
volume: number;
|
volume: number;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
|||||||
volume,
|
volume,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
isHidden,
|
||||||
onClose,
|
onClose,
|
||||||
onFooterClick,
|
onFooterClick,
|
||||||
setControlsVisible,
|
setControlsVisible,
|
||||||
@ -170,6 +172,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
|||||||
noPlay={!isActive}
|
noPlay={!isActive}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
isMuted={isMuted}
|
isMuted={isMuted}
|
||||||
|
isHidden={isHidden}
|
||||||
isProtected={isProtected}
|
isProtected={isProtected}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
playbackRate={playbackRate}
|
playbackRate={playbackRate}
|
||||||
@ -202,6 +205,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
volume,
|
volume,
|
||||||
isMuted,
|
isMuted,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
|
isHidden,
|
||||||
} = global.mediaViewer;
|
} = global.mediaViewer;
|
||||||
|
|
||||||
if (origin === MediaViewerOrigin.SearchResult) {
|
if (origin === MediaViewerOrigin.SearchResult) {
|
||||||
@ -223,6 +227,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isProtected: selectIsMessageProtected(global, message),
|
isProtected: selectIsMessageProtected(global, message),
|
||||||
volume,
|
volume,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
isHidden,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -237,6 +242,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
origin,
|
origin,
|
||||||
volume,
|
volume,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
isHidden,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -266,6 +272,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isProtected: selectIsMessageProtected(global, message),
|
isProtected: selectIsMessageProtected(global, message),
|
||||||
volume,
|
volume,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
isHidden,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -41,6 +41,7 @@ type OwnProps = {
|
|||||||
origin?: MediaViewerOrigin;
|
origin?: MediaViewerOrigin;
|
||||||
animationLevel: AnimationLevel;
|
animationLevel: AnimationLevel;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
isHidden?: boolean;
|
||||||
hasFooter?: boolean;
|
hasFooter?: boolean;
|
||||||
onFooterClick: () => void;
|
onFooterClick: () => void;
|
||||||
zoomLevelChange: number;
|
zoomLevelChange: number;
|
||||||
@ -83,6 +84,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
|||||||
hasFooter,
|
hasFooter,
|
||||||
zoomLevelChange,
|
zoomLevelChange,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
|
isHidden,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -139,7 +141,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
|||||||
useTimeout(() => setControlsVisible(true), ANIMATION_DURATION + 100);
|
useTimeout(() => setControlsVisible(true), ANIMATION_DURATION + 100);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || activeMediaId === undefined) {
|
setActiveMediaId(mediaId);
|
||||||
|
}, [mediaId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || activeMediaId === undefined || isHidden) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let lastTransform = lastTransformRef.current;
|
let lastTransform = lastTransformRef.current;
|
||||||
@ -624,10 +630,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
|||||||
clearSwipeDirectionDebounced,
|
clearSwipeDirectionDebounced,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
setIsMouseDown,
|
setIsMouseDown,
|
||||||
|
isHidden,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || !hasZoomChanged) return;
|
if (!containerRef.current || !hasZoomChanged || isHidden) return;
|
||||||
const { scale } = transformRef.current;
|
const { scale } = transformRef.current;
|
||||||
const dir = zoomLevelChange > 0 ? -1 : +1;
|
const dir = zoomLevelChange > 0 ? -1 : +1;
|
||||||
const minZoom = MIN_ZOOM * 0.6;
|
const minZoom = MIN_ZOOM * 0.6;
|
||||||
@ -655,7 +662,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
|||||||
containerRef.current.dispatchEvent(wheelEvent);
|
containerRef.current.dispatchEvent(wheelEvent);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [zoomLevelChange, hasZoomChanged]);
|
}, [zoomLevelChange, hasZoomChanged, isHidden]);
|
||||||
|
|
||||||
if (activeMediaId === undefined) return undefined;
|
if (activeMediaId === undefined) return undefined;
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { ApiDimensions } from '../../api/types';
|
|||||||
|
|
||||||
import useBuffering from '../../hooks/useBuffering';
|
import useBuffering from '../../hooks/useBuffering';
|
||||||
import useFullscreenStatus from '../../hooks/useFullscreen';
|
import useFullscreenStatus from '../../hooks/useFullscreen';
|
||||||
|
import usePictureInPicture from '../../hooks/usePictureInPicture';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
||||||
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
|
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
|
||||||
@ -31,6 +32,7 @@ type OwnProps = {
|
|||||||
noPlay?: boolean;
|
noPlay?: boolean;
|
||||||
volume: number;
|
volume: number;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
areControlsVisible: boolean;
|
areControlsVisible: boolean;
|
||||||
@ -39,6 +41,7 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MOBILE_VERSION_CONTROL_WIDTH = 400;
|
const MOBILE_VERSION_CONTROL_WIDTH = 400;
|
||||||
|
const MAX_LOOP_DURATION = 30; // Seconds
|
||||||
|
|
||||||
const VideoPlayer: FC<OwnProps> = ({
|
const VideoPlayer: FC<OwnProps> = ({
|
||||||
url,
|
url,
|
||||||
@ -61,12 +64,21 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
setMediaViewerVolume,
|
setMediaViewerVolume,
|
||||||
setMediaViewerMuted,
|
setMediaViewerMuted,
|
||||||
setMediaViewerPlaybackRate,
|
setMediaViewerPlaybackRate,
|
||||||
|
setMediaViewerHidden,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [isPlayed, setIsPlayed] = useState(!IS_TOUCH_ENV || !IS_IOS);
|
const [isPlaying, setIsPlaying] = useState(!IS_TOUCH_ENV || !IS_IOS);
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [isFullscreen, setFullscreen, exitFullscreen] = useFullscreenStatus(videoRef, setIsPlayed);
|
const [isFullscreen, setFullscreen, exitFullscreen] = useFullscreenStatus(videoRef, setIsPlaying);
|
||||||
|
|
||||||
|
const handleEnterFullscreen = useCallback(() => setMediaViewerHidden(true), [setMediaViewerHidden]);
|
||||||
|
const handleLeaveFullscreen = useCallback(() => setMediaViewerHidden(false), [setMediaViewerHidden]);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isPictureInPictureSupported,
|
||||||
|
enterPictureInPicture,
|
||||||
|
] = usePictureInPicture(videoRef, handleEnterFullscreen, handleLeaveFullscreen);
|
||||||
|
|
||||||
const handleVideoMove = useCallback(() => {
|
const handleVideoMove = useCallback(() => {
|
||||||
toggleControls(true);
|
toggleControls(true);
|
||||||
@ -90,7 +102,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderPlayButton,
|
shouldRender: shouldRenderPlayButton,
|
||||||
transitionClassNames: playButtonClassNames,
|
transitionClassNames: playButtonClassNames,
|
||||||
} = useShowTransition(IS_IOS && !isPlayed && !shouldRenderSpinner, undefined, undefined, 'slow');
|
} = useShowTransition(IS_IOS && !isPlaying && !shouldRenderSpinner, undefined, undefined, 'slow');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (noPlay || !isMediaViewerOpen) {
|
if (noPlay || !isMediaViewerOpen) {
|
||||||
@ -101,12 +113,12 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
// so we need to use `autoPlay` instead to allow pre-buffering.
|
// so we need to use `autoPlay` instead to allow pre-buffering.
|
||||||
safePlay(videoRef.current!);
|
safePlay(videoRef.current!);
|
||||||
}
|
}
|
||||||
}, [noPlay, isMediaViewerOpen, url]);
|
}, [noPlay, isMediaViewerOpen, url, setMediaViewerMuted]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoRef.current!.currentTime === videoRef.current!.duration) {
|
if (videoRef.current!.currentTime === videoRef.current!.duration) {
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
setIsPlayed(false);
|
setIsPlaying(false);
|
||||||
} else {
|
} else {
|
||||||
setCurrentTime(videoRef.current!.currentTime);
|
setCurrentTime(videoRef.current!.currentTime);
|
||||||
}
|
}
|
||||||
@ -122,14 +134,14 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const togglePlayState = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent) => {
|
const togglePlayState = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isPlayed) {
|
if (isPlaying) {
|
||||||
videoRef.current!.pause();
|
videoRef.current!.pause();
|
||||||
setIsPlayed(false);
|
setIsPlaying(false);
|
||||||
} else {
|
} else {
|
||||||
safePlay(videoRef.current!);
|
safePlay(videoRef.current!);
|
||||||
setIsPlayed(true);
|
setIsPlaying(true);
|
||||||
}
|
}
|
||||||
}, [isPlayed]);
|
}, [isPlaying]);
|
||||||
|
|
||||||
useVideoCleanup(videoRef, []);
|
useVideoCleanup(videoRef, []);
|
||||||
|
|
||||||
@ -139,7 +151,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const handleEnded = useCallback(() => {
|
const handleEnded = useCallback(() => {
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
setIsPlayed(false);
|
setIsPlaying(false);
|
||||||
toggleControls(true);
|
toggleControls(true);
|
||||||
}, [toggleControls]);
|
}, [toggleControls]);
|
||||||
|
|
||||||
@ -160,6 +172,8 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
}, [setMediaViewerVolume]);
|
}, [setMediaViewerVolume]);
|
||||||
|
|
||||||
const handleVolumeMuted = useCallback(() => {
|
const handleVolumeMuted = useCallback(() => {
|
||||||
|
// Browser requires explicit user interaction to keep video playing after unmuting
|
||||||
|
videoRef.current!.muted = !videoRef.current!.muted;
|
||||||
setMediaViewerMuted({ isMuted: !isMuted });
|
setMediaViewerMuted({ isMuted: !isMuted });
|
||||||
}, [isMuted, setMediaViewerMuted]);
|
}, [isMuted, setMediaViewerMuted]);
|
||||||
|
|
||||||
@ -185,6 +199,7 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const wrapperStyle = posterSize && `width: ${posterSize.width}px; height: ${posterSize.height}px`;
|
const wrapperStyle = posterSize && `width: ${posterSize.width}px; height: ${posterSize.height}px`;
|
||||||
const videoStyle = `background-image: url(${posterData})`;
|
const videoStyle = `background-image: url(${posterData})`;
|
||||||
|
const duration = videoRef.current?.duration || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -209,17 +224,21 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
autoPlay={IS_TOUCH_ENV}
|
autoPlay={IS_TOUCH_ENV}
|
||||||
controlsList={isProtected ? 'nodownload' : undefined}
|
controlsList={isProtected ? 'nodownload' : undefined}
|
||||||
playsInline
|
playsInline
|
||||||
loop={isGif}
|
loop={isGif || duration <= MAX_LOOP_DURATION}
|
||||||
// This is to force auto playing on mobiles
|
// This is to force autoplaying on mobiles
|
||||||
muted={isGif || isMuted}
|
muted={isGif || isMuted}
|
||||||
id="media-viewer-video"
|
id="media-viewer-video"
|
||||||
style={videoStyle}
|
style={videoStyle}
|
||||||
onPlay={IS_IOS ? () => setIsPlayed(true) : undefined}
|
onPlay={() => setIsPlaying(true)}
|
||||||
onEnded={handleEnded}
|
onEnded={handleEnded}
|
||||||
onClick={!IS_SINGLE_COLUMN_LAYOUT ? togglePlayState : undefined}
|
onClick={!IS_SINGLE_COLUMN_LAYOUT ? togglePlayState : undefined}
|
||||||
onDoubleClick={!IS_TOUCH_ENV ? handleFullscreenChange : undefined}
|
onDoubleClick={!IS_TOUCH_ENV ? handleFullscreenChange : undefined}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...bufferingHandlers}
|
{...bufferingHandlers}
|
||||||
|
onPause={(e) => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
bufferingHandlers.onPause(e);
|
||||||
|
}}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
>
|
>
|
||||||
{url && <source src={url} />}
|
{url && <source src={url} />}
|
||||||
@ -243,20 +262,22 @@ const VideoPlayer: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
{!isGif && !shouldRenderSpinner && (
|
{!isGif && !shouldRenderSpinner && (
|
||||||
<VideoPlayerControls
|
<VideoPlayerControls
|
||||||
isPlayed={isPlayed}
|
isPlaying={isPlaying}
|
||||||
bufferedRanges={bufferedRanges}
|
bufferedRanges={bufferedRanges}
|
||||||
bufferedProgress={bufferedProgress}
|
bufferedProgress={bufferedProgress}
|
||||||
isBuffered={isBuffered}
|
isBuffered={isBuffered}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
isFullscreenSupported={Boolean(setFullscreen)}
|
isFullscreenSupported={Boolean(setFullscreen)}
|
||||||
|
isPictureInPictureSupported={isPictureInPictureSupported}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
fileSize={fileSize}
|
fileSize={fileSize}
|
||||||
duration={videoRef.current ? videoRef.current.duration || 0 : 0}
|
duration={duration}
|
||||||
isVisible={areControlsVisible}
|
isVisible={areControlsVisible}
|
||||||
setVisibility={toggleControls}
|
setVisibility={toggleControls}
|
||||||
isForceMobileVersion={posterSize && posterSize.width < MOBILE_VERSION_CONTROL_WIDTH}
|
isForceMobileVersion={posterSize && posterSize.width < MOBILE_VERSION_CONTROL_WIDTH}
|
||||||
onSeek={handleSeek}
|
onSeek={handleSeek}
|
||||||
onChangeFullscreen={handleFullscreenChange}
|
onChangeFullscreen={handleFullscreenChange}
|
||||||
|
onPictureInPictureChange={enterPictureInPicture}
|
||||||
onPlayPause={togglePlayState}
|
onPlayPause={togglePlayState}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
playbackRate={playbackRate}
|
playbackRate={playbackRate}
|
||||||
|
|||||||
@ -26,8 +26,9 @@ type OwnProps = {
|
|||||||
duration: number;
|
duration: number;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
isForceMobileVersion?: boolean;
|
isForceMobileVersion?: boolean;
|
||||||
isPlayed: boolean;
|
isPlaying: boolean;
|
||||||
isFullscreenSupported: boolean;
|
isFullscreenSupported: boolean;
|
||||||
|
isPictureInPictureSupported: boolean;
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isBuffered: boolean;
|
isBuffered: boolean;
|
||||||
@ -35,6 +36,7 @@ type OwnProps = {
|
|||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
onChangeFullscreen: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onChangeFullscreen: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
|
onPictureInPictureChange?: () => void ;
|
||||||
onVolumeClick: () => void;
|
onVolumeClick: () => void;
|
||||||
onVolumeChange: (volume: number) => void;
|
onVolumeChange: (volume: number) => void;
|
||||||
onPlaybackRateChange: (playbackRate: number) => void;
|
onPlaybackRateChange: (playbackRate: number) => void;
|
||||||
@ -63,7 +65,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
|||||||
duration,
|
duration,
|
||||||
fileSize,
|
fileSize,
|
||||||
isForceMobileVersion,
|
isForceMobileVersion,
|
||||||
isPlayed,
|
isPlaying,
|
||||||
isFullscreenSupported,
|
isFullscreenSupported,
|
||||||
isFullscreen,
|
isFullscreen,
|
||||||
isVisible,
|
isVisible,
|
||||||
@ -75,6 +77,8 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
|||||||
onVolumeClick,
|
onVolumeClick,
|
||||||
onVolumeChange,
|
onVolumeChange,
|
||||||
onPlaybackRateChange,
|
onPlaybackRateChange,
|
||||||
|
isPictureInPictureSupported,
|
||||||
|
onPictureInPictureChange,
|
||||||
onPlayPause,
|
onPlayPause,
|
||||||
setVisibility,
|
setVisibility,
|
||||||
onSeek,
|
onSeek,
|
||||||
@ -88,7 +92,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!IS_TOUCH_ENV) return undefined;
|
if (!IS_TOUCH_ENV) return undefined;
|
||||||
let timeout: number | undefined;
|
let timeout: number | undefined;
|
||||||
if (!isVisible || !isPlayed || isSeeking || isPlaybackMenuOpen) {
|
if (!isVisible || !isPlaying || isSeeking || isPlaybackMenuOpen) {
|
||||||
if (timeout) window.clearTimeout(timeout);
|
if (timeout) window.clearTimeout(timeout);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -98,7 +102,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
if (timeout) window.clearTimeout(timeout);
|
if (timeout) window.clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
}, [isPlayed, isVisible, isSeeking, setVisibility, isPlaybackMenuOpen]);
|
}, [isPlaying, isVisible, isSeeking, setVisibility, isPlaybackMenuOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
@ -172,7 +176,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
|||||||
round
|
round
|
||||||
onClick={onPlayPause}
|
onClick={onPlayPause}
|
||||||
>
|
>
|
||||||
<i className={isPlayed ? 'icon-pause' : 'icon-play'} />
|
<i className={isPlaying ? 'icon-pause' : 'icon-play'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
ariaLabel="Volume"
|
ariaLabel="Volume"
|
||||||
@ -204,6 +208,18 @@ const VideoPlayerControls: FC<OwnProps> = ({
|
|||||||
>
|
>
|
||||||
{`${playbackRate}x`}
|
{`${playbackRate}x`}
|
||||||
</Button>
|
</Button>
|
||||||
|
{isPictureInPictureSupported && (
|
||||||
|
<Button
|
||||||
|
ariaLabel="Picture in picture"
|
||||||
|
size="tiny"
|
||||||
|
color="translucent-white"
|
||||||
|
className="fullscreen"
|
||||||
|
round
|
||||||
|
onClick={onPictureInPictureChange}
|
||||||
|
>
|
||||||
|
<i className="icon-pip" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{isFullscreenSupported && (
|
{isFullscreenSupported && (
|
||||||
<Button
|
<Button
|
||||||
ariaLabel="Fullscreen"
|
ariaLabel="Fullscreen"
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import buildClassName from '../../util/buildClassName';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
onClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||||
@ -17,18 +18,19 @@ type OwnProps = {
|
|||||||
|
|
||||||
const ShowTransition: FC<OwnProps> = ({
|
const ShowTransition: FC<OwnProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
isHidden,
|
||||||
isCustom,
|
isCustom,
|
||||||
id,
|
id,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
noCloseTransition,
|
|
||||||
children,
|
children,
|
||||||
|
noCloseTransition,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
shouldRender,
|
shouldRender,
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
} = useShowTransition(
|
} = useShowTransition(
|
||||||
isOpen, undefined, undefined, isCustom ? false : undefined, noCloseTransition,
|
isOpen && !isHidden, undefined, undefined, isCustom ? false : undefined, noCloseTransition,
|
||||||
);
|
);
|
||||||
const prevIsOpen = usePrevious(isOpen);
|
const prevIsOpen = usePrevious(isOpen);
|
||||||
const prevChildren = usePrevious(children);
|
const prevChildren = usePrevious(children);
|
||||||
@ -39,7 +41,7 @@ const ShowTransition: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
shouldRender && (
|
(shouldRender || isHidden) && (
|
||||||
<div id={id} className={buildClassName(className, transitionClassNames)} onClick={onClick}>
|
<div id={id} className={buildClassName(className, transitionClassNames)} onClick={onClick}>
|
||||||
{isOpen ? children : fromChildrenRef.current!}
|
{isOpen ? children : fromChildrenRef.current!}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ addActionHandler('openMediaViewer', (global, actions, payload) => {
|
|||||||
avatarOwnerId,
|
avatarOwnerId,
|
||||||
profilePhotoIndex,
|
profilePhotoIndex,
|
||||||
origin,
|
origin,
|
||||||
|
isHidden: false,
|
||||||
volume: volume ?? global.mediaViewer.volume,
|
volume: volume ?? global.mediaViewer.volume,
|
||||||
playbackRate: playbackRate || global.mediaViewer.playbackRate,
|
playbackRate: playbackRate || global.mediaViewer.playbackRate,
|
||||||
isMuted: isMuted || global.mediaViewer.isMuted,
|
isMuted: isMuted || global.mediaViewer.isMuted,
|
||||||
@ -24,12 +25,15 @@ addActionHandler('openMediaViewer', (global, actions, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('closeMediaViewer', (global) => {
|
addActionHandler('closeMediaViewer', (global) => {
|
||||||
const { volume, isMuted, playbackRate } = global.mediaViewer;
|
const {
|
||||||
|
volume, isMuted, playbackRate, isHidden,
|
||||||
|
} = global.mediaViewer;
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
mediaViewer: {
|
mediaViewer: {
|
||||||
volume,
|
volume,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
isHidden,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -77,3 +81,15 @@ addActionHandler('setMediaViewerMuted', (global, actions, payload) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('setMediaViewerHidden', (global, actions, payload) => {
|
||||||
|
const isHidden = payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
mediaViewer: {
|
||||||
|
...global.mediaViewer,
|
||||||
|
isHidden,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@ -425,6 +425,7 @@ export type GlobalState = {
|
|||||||
volume: number;
|
volume: number;
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
audioPlayer: {
|
audioPlayer: {
|
||||||
@ -803,7 +804,7 @@ export interface ActionPayloads {
|
|||||||
setMediaViewerMuted: {
|
setMediaViewerMuted: {
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
};
|
};
|
||||||
|
setMediaViewerHidden: boolean;
|
||||||
openAudioPlayer: {
|
openAudioPlayer: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
threadId?: number;
|
threadId?: number;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useLayoutEffect, useState } from '../lib/teact/teact';
|
import { useLayoutEffect, useState } from '../lib/teact/teact';
|
||||||
import { PLATFORM_ENV } from '../util/environment';
|
import { IS_IOS } from '../util/environment';
|
||||||
|
|
||||||
type RefType = {
|
type RefType = {
|
||||||
current: HTMLElement | null;
|
current: HTMLVideoElement | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReturnType = [boolean, () => void, () => void] | [false];
|
type ReturnType = [boolean, () => void, () => void] | [false];
|
||||||
@ -14,20 +14,10 @@ export default function useFullscreenStatus(elRef: RefType, setIsPlayed: Callbac
|
|||||||
const [isFullscreen, setIsFullscreen] = useState(Boolean(prop && document[prop]));
|
const [isFullscreen, setIsFullscreen] = useState(Boolean(prop && document[prop]));
|
||||||
|
|
||||||
const setFullscreen = () => {
|
const setFullscreen = () => {
|
||||||
if (!elRef.current || !(prop || PLATFORM_ENV === 'iOS')) {
|
if (!elRef.current || !(prop || IS_IOS)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
safeRequestFullscreen(elRef.current);
|
||||||
if (elRef.current.requestFullscreen) {
|
|
||||||
elRef.current.requestFullscreen();
|
|
||||||
} else if (elRef.current.webkitRequestFullscreen) {
|
|
||||||
elRef.current.webkitRequestFullscreen();
|
|
||||||
} else if (elRef.current.webkitEnterFullscreen) {
|
|
||||||
elRef.current.webkitEnterFullscreen();
|
|
||||||
} else if (elRef.current.mozRequestFullScreen) {
|
|
||||||
elRef.current.mozRequestFullScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFullscreen(true);
|
setIsFullscreen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,17 +25,7 @@ export default function useFullscreenStatus(elRef: RefType, setIsPlayed: Callbac
|
|||||||
if (!elRef.current) {
|
if (!elRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
safeExitFullscreen();
|
||||||
if (document.exitFullscreen) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
} else if (document.mozCancelFullScreen) {
|
|
||||||
document.mozCancelFullScreen();
|
|
||||||
} else if (document.webkitCancelFullScreen) {
|
|
||||||
document.webkitCancelFullScreen();
|
|
||||||
} else if (document.webkitExitFullscreen) {
|
|
||||||
document.webkitExitFullscreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFullscreen(false);
|
setIsFullscreen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,7 +59,7 @@ export default function useFullscreenStatus(elRef: RefType, setIsPlayed: Callbac
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!prop && PLATFORM_ENV !== 'iOS') {
|
if (!prop && !IS_IOS) {
|
||||||
return [false];
|
return [false];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,3 +77,27 @@ function getBrowserFullscreenElementProp() {
|
|||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function safeRequestFullscreen(video: HTMLVideoElement) {
|
||||||
|
if (video.requestFullscreen) {
|
||||||
|
video.requestFullscreen();
|
||||||
|
} else if (video.webkitRequestFullscreen) {
|
||||||
|
video.webkitRequestFullscreen();
|
||||||
|
} else if (video.webkitEnterFullscreen) {
|
||||||
|
video.webkitEnterFullscreen();
|
||||||
|
} else if (video.mozRequestFullScreen) {
|
||||||
|
video.mozRequestFullScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeExitFullscreen() {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if (document.mozCancelFullScreen) {
|
||||||
|
document.mozCancelFullScreen();
|
||||||
|
} else if (document.webkitCancelFullScreen) {
|
||||||
|
document.webkitCancelFullScreen();
|
||||||
|
} else if (document.webkitExitFullscreen) {
|
||||||
|
document.webkitExitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
107
src/hooks/usePictureInPicture.ts
Normal file
107
src/hooks/usePictureInPicture.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { useLayoutEffect, useCallback, useState } from '../lib/teact/teact';
|
||||||
|
import { DEBUG } from '../config';
|
||||||
|
import { IS_IOS, IS_PWA } from '../util/environment';
|
||||||
|
import safePlay, { getIsVideoPlaying } from '../util/safePlay';
|
||||||
|
|
||||||
|
type RefType = {
|
||||||
|
current: HTMLVideoElement | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReturnType = [boolean, () => void] | [false];
|
||||||
|
type CallbackType = () => void;
|
||||||
|
|
||||||
|
export default function usePictureInPicture(
|
||||||
|
elRef: RefType,
|
||||||
|
onEnter: CallbackType,
|
||||||
|
onLeave: CallbackType,
|
||||||
|
): ReturnType {
|
||||||
|
const [isSupported, setIsSupported] = useState(false);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// PIP is not supported in PWA on iOS, despite being detected
|
||||||
|
if ((IS_IOS && IS_PWA) || !elRef.current) return undefined;
|
||||||
|
const video = elRef.current;
|
||||||
|
const setMode = getSetPresentationMode(video);
|
||||||
|
const isEnabled = (document.pictureInPictureEnabled && !elRef.current?.disablePictureInPicture)
|
||||||
|
|| setMode !== undefined;
|
||||||
|
if (!isEnabled) return undefined;
|
||||||
|
// @ts-ignore
|
||||||
|
video.autoPictureInPicture = true;
|
||||||
|
setIsSupported(true);
|
||||||
|
video.addEventListener('enterpictureinpicture', onEnter);
|
||||||
|
video.addEventListener('leavepictureinpicture', onLeave);
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('enterpictureinpicture', onEnter);
|
||||||
|
video.removeEventListener('leavepictureinpicture', onLeave);
|
||||||
|
};
|
||||||
|
}, [elRef, onEnter, onLeave]);
|
||||||
|
|
||||||
|
const exitPictureInPicture = useCallback(() => {
|
||||||
|
if (!elRef.current) return;
|
||||||
|
const video = elRef.current;
|
||||||
|
const setMode = getSetPresentationMode(video);
|
||||||
|
if (setMode) {
|
||||||
|
setMode('inline');
|
||||||
|
} else {
|
||||||
|
exitPictureInPictureIfNeeded();
|
||||||
|
}
|
||||||
|
}, [elRef]);
|
||||||
|
|
||||||
|
const enterPictureInPicture = useCallback(() => {
|
||||||
|
if (!elRef.current) return;
|
||||||
|
exitPictureInPicture();
|
||||||
|
const video = elRef.current;
|
||||||
|
const isPlaying = getIsVideoPlaying(video);
|
||||||
|
const setMode = getSetPresentationMode(video);
|
||||||
|
if (setMode) {
|
||||||
|
setMode('picture-in-picture');
|
||||||
|
} else {
|
||||||
|
requestPictureInPicture(video);
|
||||||
|
}
|
||||||
|
// Muted video stops in PiP mode, so we need to play it again
|
||||||
|
if (isPlaying) {
|
||||||
|
safePlay(video);
|
||||||
|
}
|
||||||
|
}, [elRef, exitPictureInPicture]);
|
||||||
|
|
||||||
|
if (!isSupported) {
|
||||||
|
return [false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [isSupported, enterPictureInPicture];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSetPresentationMode(video: HTMLVideoElement) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') {
|
||||||
|
// @ts-ignore
|
||||||
|
return video.webkitSetPresentationMode.bind(video);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPictureInPicture(video: HTMLVideoElement) {
|
||||||
|
if (video.requestPictureInPicture) {
|
||||||
|
try {
|
||||||
|
video.requestPictureInPicture();
|
||||||
|
} catch (err) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[MV] PictureInPicture Error', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitPictureInPictureIfNeeded() {
|
||||||
|
if (document.pictureInPictureElement) {
|
||||||
|
try {
|
||||||
|
document.exitPictureInPicture();
|
||||||
|
} catch (err) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[MV] PictureInPicture Error', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -51,6 +51,9 @@
|
|||||||
.icon-volume-3:before {
|
.icon-volume-3:before {
|
||||||
content: "\e991";
|
content: "\e991";
|
||||||
}
|
}
|
||||||
|
.icon-pip:before {
|
||||||
|
content: "\e9ae";
|
||||||
|
}
|
||||||
.icon-gift:before {
|
.icon-gift:before {
|
||||||
content: "\e9ad";
|
content: "\e9ad";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,12 @@ export function getPlatform() {
|
|||||||
const iosPlatforms = ['iPhone', 'iPad', 'iPod'];
|
const iosPlatforms = ['iPhone', 'iPad', 'iPod'];
|
||||||
let os: 'macOS' | 'iOS' | 'Windows' | 'Android' | 'Linux' | undefined;
|
let os: 'macOS' | 'iOS' | 'Windows' | 'Android' | 'Linux' | undefined;
|
||||||
|
|
||||||
if (macosPlatforms.indexOf(platform) !== -1) {
|
if (iosPlatforms.indexOf(platform) !== -1
|
||||||
os = 'macOS';
|
// For new IPads with M1 chip and IPadOS platform returns "MacIntel"
|
||||||
} else if (iosPlatforms.indexOf(platform) !== -1) {
|
|| (platform === 'MacIntel' && ('maxTouchPoints' in navigator && navigator.maxTouchPoints > 2))) {
|
||||||
os = 'iOS';
|
os = 'iOS';
|
||||||
|
} else if (macosPlatforms.indexOf(platform) !== -1) {
|
||||||
|
os = 'macOS';
|
||||||
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
||||||
os = 'Windows';
|
os = 'Windows';
|
||||||
} else if (/Android/.test(userAgent)) {
|
} else if (/Android/.test(userAgent)) {
|
||||||
|
|||||||
@ -9,4 +9,8 @@ const safePlay = (mediaEl: HTMLMediaElement) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getIsVideoPlaying = (video: HTMLVideoElement) => {
|
||||||
|
return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > 2;
|
||||||
|
};
|
||||||
|
|
||||||
export default safePlay;
|
export default safePlay;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user