import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo, useLayoutEffect, } from '../../lib/teact/teact'; import type { BufferedRange } from '../../hooks/useBuffering'; import type { ApiDimensions } from '../../api/types'; import useLastCallback from '../../hooks/useLastCallback'; import useLang from '../../hooks/useLang'; import useFlag from '../../hooks/useFlag'; import useAppLayout from '../../hooks/useAppLayout'; import useDerivedState from '../../hooks/useDerivedState'; import useSignal from '../../hooks/useSignal'; import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal'; import useControlsSignal from './hooks/useControlsSignal'; import buildClassName from '../../util/buildClassName'; import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { formatMediaDuration } from '../../util/dateFormat'; import { formatFileSize } from '../../util/textFormat'; import SeekLine from './SeekLine'; 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 = { url?: string; bufferedRanges: BufferedRange[]; bufferedProgress: number; duration: number; isReady: boolean; fileSize: number; isForceMobileVersion?: boolean; isPlaying: boolean; isFullscreenSupported: boolean; isPictureInPictureSupported: boolean; isFullscreen: boolean; isPreviewDisabled?: boolean; isBuffered: boolean; volume: number; isMuted: boolean; playbackRate: number; posterSize?: ApiDimensions; onChangeFullscreen: (e: React.MouseEvent) => void; onPictureInPictureChange?: () => void; onVolumeClick: () => void; onVolumeChange: (volume: number) => void; onPlaybackRateChange: (playbackRate: number) => void; onPlayPause: (e: React.MouseEvent) => 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 = 3000; const VideoPlayerControls: FC = ({ url, bufferedRanges, bufferedProgress, duration, isReady, fileSize, isForceMobileVersion, isPlaying, isFullscreenSupported, isFullscreen, isBuffered, isPreviewDisabled, volume, isMuted, playbackRate, posterSize, onChangeFullscreen, onVolumeClick, onVolumeChange, onPlaybackRateChange, isPictureInPictureSupported, onPictureInPictureChange, onPlayPause, onSeek, }) => { const [isPlaybackMenuOpen, openPlaybackMenu, closePlaybackMenu] = useFlag(); const [getCurrentTime] = useCurrentTimeSignal(); const currentTime = useDerivedState(() => Math.trunc(getCurrentTime()), [getCurrentTime]); const [getIsSeeking, setIsSeeking] = useSignal(false); const { isMobile } = useAppLayout(); const [getIsVisible, setVisibility] = useControlsSignal(); const isVisible = useDerivedState(getIsVisible); useEffect(() => { if (!IS_TOUCH_ENV && !isForceMobileVersion) return undefined; let timeout: number | undefined; if (!isVisible || !isPlaying || isPlaybackMenuOpen || getIsSeeking()) { if (timeout) window.clearTimeout(timeout); return undefined; } timeout = window.setTimeout(() => { setVisibility(false); }, HIDE_CONTROLS_TIMEOUT_MS); return () => { if (timeout) window.clearTimeout(timeout); }; }, [isPlaying, isVisible, setVisibility, isPlaybackMenuOpen, getIsSeeking, isForceMobileVersion]); useLayoutEffect(() => { 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 = useLastCallback((position: number) => { setIsSeeking(false); onSeek(position); }); const handleSeekStart = useLastCallback(() => { setIsSeeking(true); }); 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 (
{!IS_IOS && ( )} {renderTime(currentTime, duration)} {!isBuffered && (
{`${formatFileSize(lang, fileSize * bufferedProgress)} / ${formatFileSize(lang, fileSize)}`}
)}
{isPictureInPictureSupported && ( )} {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)}`}
); } export default memo(VideoPlayerControls);