import React, { FC, useEffect, useRef, useCallback, useMemo, } from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; import useFlag from '../../hooks/useFlag'; import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; import { formatMediaDuration } from '../../util/dateFormat'; import formatFileSize from './helpers/formatFileSize'; import useLang from '../../hooks/useLang'; import { BufferedRange } from '../../hooks/useBuffering'; import { captureEvents } from '../../util/captureEvents'; import Button from '../ui/Button'; import RangeSlider from '../ui/RangeSlider'; import Menu from '../ui/Menu'; import MenuItem from '../ui/MenuItem'; import './VideoPlayerControls.scss'; type OwnProps = { bufferedRanges: BufferedRange[]; bufferedProgress: number; currentTime: number; duration: number; fileSize: number; isForceMobileVersion?: boolean; isPlayed: boolean; isFullscreenSupported: boolean; isFullscreen: boolean; isVisible: boolean; isBuffered: boolean; volume: number; isMuted: boolean; playbackRate: number; onChangeFullscreen: (e: React.MouseEvent) => void; onVolumeClick: () => void; onVolumeChange: (volume: number) => void; onPlaybackRateChange: (playbackRate: number) => void; onPlayPause: (e: React.MouseEvent) => void; setVisibility: (isVisible: boolean) => void; onSeek: (position: number) => void; }; const stopEvent = (e: React.MouseEvent) => { e.stopPropagation(); }; const PLAYBACK_RATES = [ 0.5, 1, 1.5, 2, ]; const HIDE_CONTROLS_TIMEOUT_MS = 1500; const VideoPlayerControls: FC = ({ bufferedRanges, bufferedProgress, currentTime, duration, fileSize, isForceMobileVersion, isPlayed, isFullscreenSupported, isFullscreen, isVisible, isBuffered, volume, isMuted, playbackRate, onChangeFullscreen, onVolumeClick, onVolumeChange, onPlaybackRateChange, onPlayPause, setVisibility, onSeek, }) => { const [isPlaybackMenuOpen, openPlaybackMenu, closePlaybackMenu] = useFlag(); // eslint-disable-next-line no-null/no-null const seekerRef = useRef(null); const isSeekingRef = useRef(false); const isSeeking = isSeekingRef.current; useEffect(() => { let timeout: number | undefined; if (!isVisible || !isPlayed || isSeeking || isPlaybackMenuOpen) { if (timeout) window.clearTimeout(timeout); return undefined; } timeout = window.setTimeout(() => { setVisibility(false); }, HIDE_CONTROLS_TIMEOUT_MS); return () => { if (timeout) window.clearTimeout(timeout); }; }, [isPlayed, isVisible, isSeeking, setVisibility, isPlaybackMenuOpen]); useEffect(() => { if (isVisible) { document.body.classList.add('video-controls-visible'); } else { document.body.classList.remove('video-controls-visible'); } return () => { document.body.classList.remove('video-controls-visible'); }; }, [isVisible]); useEffect(() => { if (!isVisible) { closePlaybackMenu(); } }, [closePlaybackMenu, isVisible]); const lang = useLang(); const handleSeek = useCallback((e: MouseEvent | TouchEvent) => { if (isSeekingRef.current && seekerRef.current) { const { width, left, } = seekerRef.current.getBoundingClientRect(); const clientX = e instanceof MouseEvent ? e.clientX : e.targetTouches[0].clientX; onSeek(Math.max(Math.min(duration * ((clientX - left) / width), duration), 0)); } }, [duration, onSeek]); const handleStartSeek = useCallback((e: MouseEvent | TouchEvent) => { isSeekingRef.current = true; handleSeek(e); }, [handleSeek]); const handleStopSeek = useCallback(() => { isSeekingRef.current = false; }, []); useEffect(() => { if (!seekerRef.current || !isVisible) return undefined; return captureEvents(seekerRef.current, { onCapture: handleStartSeek, onRelease: handleStopSeek, onClick: handleStopSeek, onDrag: handleSeek, }); }, [isVisible, handleStartSeek, handleSeek, handleStopSeek]); const volumeIcon = useMemo(() => { if (volume === 0 || isMuted) return 'icon-muted'; if (volume < 0.3) return 'icon-volume-1'; if (volume < 0.6) return 'icon-volume-2'; return 'icon-volume-3'; }, [volume, isMuted]); return (
{renderSeekLine(currentTime, duration, bufferedRanges, seekerRef)}
{!IS_IOS && ( )} {renderTime(currentTime, duration)} {!isBuffered && renderFileSize(bufferedProgress, fileSize)}
{isFullscreenSupported && ( )}
{PLAYBACK_RATES.map((rate) => ( // eslint-disable-next-line react/jsx-no-bind onPlaybackRateChange(rate)}> {`${rate}x`} ))}
); }; function renderTime(currentTime: number, duration: number) { return (
{`${formatMediaDuration(currentTime)} / ${formatMediaDuration(duration)}`}
); } function renderFileSize(loadedPercent: number, totalSize: number) { return (
{`${formatFileSize(totalSize * loadedPercent)} / ${formatFileSize(totalSize)}`}
); } function renderSeekLine( currentTime: number, duration: number, bufferedRanges: BufferedRange[], seekerRef: React.RefObject, ) { const percentagePlayed = (currentTime / duration) * 100; return (
{bufferedRanges.map(({ start, end }) => (
))}
); } export default VideoPlayerControls;