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) => void; }; const MOBILE_VERSION_CONTROL_WIDTH = 400; const VideoPlayer: FC = ({ 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(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 | 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) => { if (e.target === videoRef.current) { toggleControls(false); } }, [toggleControls]); const handleTimeUpdate = useCallback((e: React.SyntheticEvent) => { 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 (
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
{shouldRenderPlayButton && ( )} {shouldRenderSpinner && (
{!isBuffered &&
Buffering...
}
)} {!isGif && !shouldRenderSpinner && ( )}
); }; export default memo(VideoPlayer);