2022-03-19 21:19:59 +01:00

261 lines
8.1 KiB
TypeScript

import React, {
FC, memo, useCallback, useEffect, useRef, useState,
} from '../../lib/teact/teact';
import { getActions } from '../../global';
import { ApiDimensions } from '../../api/types';
import useBuffering from '../../hooks/useBuffering';
import useFullscreenStatus from '../../hooks/useFullscreen';
import useShowTransition from '../../hooks/useShowTransition';
import useVideoCleanup from '../../hooks/useVideoCleanup';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import safePlay from '../../util/safePlay';
import Button from '../ui/Button';
import ProgressSpinner from '../ui/ProgressSpinner';
import './VideoPlayer.scss';
import VideoPlayerControls from './VideoPlayerControls';
type OwnProps = {
url?: string;
isGif?: boolean;
posterData?: string;
posterSize?: ApiDimensions;
loadProgress?: number;
fileSize: number;
isMediaViewerOpen?: boolean;
noPlay?: boolean;
areControlsVisible: boolean;
volume: number;
isMuted: boolean;
playbackRate: number;
toggleControls: (isVisible: boolean) => void;
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
};
const MOBILE_VERSION_CONTROL_WIDTH = 400;
const VideoPlayer: FC<OwnProps> = ({
url,
isGif,
posterData,
posterSize,
loadProgress,
fileSize,
isMediaViewerOpen,
noPlay,
volume,
isMuted,
playbackRate,
onClose,
toggleControls,
areControlsVisible,
}) => {
const {
setMediaViewerVolume,
setMediaViewerMuted,
setMediaViewerPlaybackRate,
} = getActions();
// eslint-disable-next-line no-null/no-null
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlayed, setIsPlayed] = useState(!IS_TOUCH_ENV || !IS_IOS);
const [currentTime, setCurrentTime] = useState(0);
const [isFullscreen, setFullscreen, exitFullscreen] = useFullscreenStatus(videoRef, setIsPlayed);
const {
isBuffered, bufferedRanges, bufferingHandlers, bufferedProgress,
} = useBuffering();
const {
shouldRender: shouldRenderSpinner,
transitionClassNames: spinnerClassNames,
} = useShowTransition(!isBuffered, undefined, undefined, 'slow');
const {
shouldRender: shouldRenderPlayButton,
transitionClassNames: playButtonClassNames,
} = useShowTransition(IS_IOS && !isPlayed && !shouldRenderSpinner, undefined, undefined, 'slow');
useEffect(() => {
if (noPlay || !isMediaViewerOpen) {
videoRef.current!.pause();
} else if (url && !IS_TOUCH_ENV) {
// Chrome does not automatically start playing when `url` becomes available (even with `autoPlay`),
// so we force it here. Contrary, iOS does not allow to call `play` without mouse event,
// so we need to use `autoPlay` instead to allow pre-buffering.
safePlay(videoRef.current!);
}
}, [noPlay, isMediaViewerOpen, url]);
useEffect(() => {
if (videoRef.current!.currentTime === videoRef.current!.duration) {
setCurrentTime(0);
setIsPlayed(false);
} else {
setCurrentTime(videoRef.current!.currentTime);
}
}, [currentTime]);
useEffect(() => {
videoRef.current!.volume = volume;
}, [volume]);
useEffect(() => {
videoRef.current!.playbackRate = playbackRate;
}, [playbackRate]);
const togglePlayState = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent) => {
e.stopPropagation();
if (isPlayed) {
videoRef.current!.pause();
setIsPlayed(false);
} else {
safePlay(videoRef.current!);
setIsPlayed(true);
}
}, [isPlayed]);
useVideoCleanup(videoRef, []);
const handleMouseMove = useCallback(() => {
toggleControls(true);
}, [toggleControls]);
const handleMouseOut = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
if (e.target === videoRef.current) {
toggleControls(false);
}
}, [toggleControls]);
const handleTimeUpdate = useCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
setCurrentTime(e.currentTarget.currentTime);
}, []);
const handleEnded = useCallback(() => {
setCurrentTime(0);
setIsPlayed(false);
toggleControls(true);
}, [toggleControls]);
const handleFullscreenChange = useCallback(() => {
if (isFullscreen && exitFullscreen) {
exitFullscreen();
} else if (!isFullscreen && setFullscreen) {
setFullscreen();
}
}, [exitFullscreen, isFullscreen, setFullscreen]);
const handleSeek = useCallback((position: number) => {
videoRef.current!.currentTime = position;
}, []);
const handleVolumeChange = useCallback((newVolume: number) => {
setMediaViewerVolume({ volume: newVolume / 100 });
}, [setMediaViewerVolume]);
const handleVolumeMuted = useCallback(() => {
setMediaViewerMuted({ isMuted: !isMuted });
}, [isMuted, setMediaViewerMuted]);
const handlePlaybackRateChange = useCallback((newPlaybackRate: number) => {
setMediaViewerPlaybackRate({ playbackRate: newPlaybackRate });
}, [setMediaViewerPlaybackRate]);
useEffect(() => {
if (!isMediaViewerOpen) return undefined;
const togglePayingStateBySpace = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
togglePlayState(e);
}
};
document.addEventListener('keydown', togglePayingStateBySpace, false);
return () => {
document.removeEventListener('keydown', togglePayingStateBySpace, false);
};
}, [togglePlayState, isMediaViewerOpen]);
const wrapperStyle = posterSize && `width: ${posterSize.width}px; height: ${posterSize.height}px`;
const videoStyle = `background-image: url(${posterData})`;
return (
<div
className="VideoPlayer"
onMouseMove={!isGif && !IS_TOUCH_ENV ? handleMouseMove : undefined}
onMouseOut={!isGif && !IS_TOUCH_ENV ? handleMouseOut : undefined}
>
<div
style={wrapperStyle}
>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
ref={videoRef}
autoPlay={IS_TOUCH_ENV}
playsInline
loop={isGif}
// This is to force auto playing on mobiles
muted={isGif || isMuted}
id="media-viewer-video"
style={videoStyle}
onPlay={IS_IOS ? () => setIsPlayed(true) : undefined}
onEnded={handleEnded}
onClick={!IS_SINGLE_COLUMN_LAYOUT ? togglePlayState : undefined}
onDoubleClick={!IS_TOUCH_ENV ? handleFullscreenChange : undefined}
// eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers}
onTimeUpdate={handleTimeUpdate}
>
{url && <source src={url} />}
</video>
</div>
{shouldRenderPlayButton && (
<Button round className={`play-button ${playButtonClassNames}`} onClick={togglePlayState}>
<i className="icon-play" />
</Button>
)}
{shouldRenderSpinner && (
<div className={['spinner-container', spinnerClassNames].join(' ')}>
{!isBuffered && <div className="buffering">Buffering...</div>}
<ProgressSpinner
size="xl"
progress={isBuffered ? 1 : loadProgress}
square
onClick={onClose}
/>
</div>
)}
{!isGif && !shouldRenderSpinner && (
<VideoPlayerControls
isPlayed={isPlayed}
bufferedRanges={bufferedRanges}
bufferedProgress={bufferedProgress}
isBuffered={isBuffered}
currentTime={currentTime}
isFullscreenSupported={Boolean(setFullscreen)}
isFullscreen={isFullscreen}
fileSize={fileSize}
duration={videoRef.current ? videoRef.current.duration || 0 : 0}
isVisible={areControlsVisible}
setVisibility={toggleControls}
isForceMobileVersion={posterSize && posterSize.width < MOBILE_VERSION_CONTROL_WIDTH}
onSeek={handleSeek}
onChangeFullscreen={handleFullscreenChange}
onPlayPause={togglePlayState}
volume={volume}
playbackRate={playbackRate}
isMuted={isMuted}
onVolumeClick={handleVolumeMuted}
onVolumeChange={handleVolumeChange}
onPlaybackRateChange={handlePlaybackRateChange}
/>
)}
</div>
);
};
export default memo(VideoPlayer);