mastodon/app/javascript/flavours/glitch/features/video/index.tsx
Eugen Rochko da5a957d8f [Glitch] Fix being unable to hide controls in full screen video in web UI
Port 70e14c1ed0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-03-31 17:16:38 +02:00

1051 lines
30 KiB
TypeScript

import { useEffect, useCallback, useRef, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useSpring, animated, config } from '@react-spring/web';
import { throttle } from 'lodash';
import Forward5Icon from '@/material-icons/400-24px/forward_5-fill.svg?react';
import FullscreenIcon from '@/material-icons/400-24px/fullscreen.svg?react';
import FullscreenExitIcon from '@/material-icons/400-24px/fullscreen_exit.svg?react';
import PauseIcon from '@/material-icons/400-24px/pause-fill.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
import RectangleIcon from '@/material-icons/400-24px/rectangle.svg?react';
import Replay5Icon from '@/material-icons/400-24px/replay_5-fill.svg?react';
import VolumeDownIcon from '@/material-icons/400-24px/volume_down-fill.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { SpoilerButton } from 'flavours/glitch/components/spoiler_button';
import {
isFullscreen,
requestFullscreen,
exitFullscreen,
attachFullscreenListener,
detachFullscreenListener,
} from 'flavours/glitch/features/ui/util/fullscreen';
import {
displayMedia,
useBlurhash,
reduceMotion,
} from 'flavours/glitch/initial_state';
import { playerSettings } from 'flavours/glitch/settings';
import { HotkeyIndicator } from './components/hotkey_indicator';
import type { HotkeyEvent } from './components/hotkey_indicator';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute' },
hide: { id: 'video.hide', defaultMessage: 'Hide video' },
expand: { id: 'video.expand', defaultMessage: 'Expand video' },
close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: {
id: 'video.exit_fullscreen',
defaultMessage: 'Exit full screen',
},
volumeUp: { id: 'video.volume_up', defaultMessage: 'Volume up' },
volumeDown: { id: 'video.volume_down', defaultMessage: 'Volume down' },
skipForward: { id: 'video.skip_forward', defaultMessage: 'Skip forward' },
skipBackward: { id: 'video.skip_backward', defaultMessage: 'Skip backward' },
});
const DOUBLE_CLICK_THRESHOLD = 250;
const HOVER_FADE_DELAY = 4000;
export const formatTime = (secondsNum: number) => {
const hours = Math.floor(secondsNum / 3600);
const minutes = Math.floor((secondsNum - hours * 3600) / 60);
const seconds = secondsNum - hours * 3600 - minutes * 60;
const formattedHours = `${hours < 10 ? '0' : ''}${hours}`;
const formattedMinutes = `${minutes < 10 ? '0' : ''}${minutes}`;
const formattedSeconds = `${seconds < 10 ? '0' : ''}${seconds}`;
return (
(formattedHours === '00' ? '' : `${formattedHours}:`) +
`${formattedMinutes}:${formattedSeconds}`
);
};
export const findElementPosition = (el: HTMLElement) => {
const box = el.getBoundingClientRect();
const docEl = document.documentElement;
const body = document.body;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const scrollLeft = window.scrollX || body.scrollLeft;
const left = box.left + scrollLeft - clientLeft;
const clientTop = docEl.clientTop || body.clientTop || 0;
const scrollTop = window.scrollY || body.scrollTop;
const top = box.top + scrollTop - clientTop;
return {
left: Math.round(left),
top: Math.round(top),
};
};
export const getPointerPosition = (
el: HTMLElement | null,
event: MouseEvent,
) => {
if (!el) {
return {
y: 0,
x: 0,
};
}
const box = findElementPosition(el);
const boxW = el.offsetWidth;
const boxH = el.offsetHeight;
const boxY = box.top;
const boxX = box.left;
const { pageY, pageX } = event;
return {
y: Math.max(0, Math.min(1, (pageY - boxY) / boxH)),
x: Math.max(0, Math.min(1, (pageX - boxX) / boxW)),
};
};
export const fileNameFromURL = (str: string) => {
const url = new URL(str);
const pathname = url.pathname;
const index = pathname.lastIndexOf('/');
return pathname.slice(index + 1);
};
const frameRateAsNumber = (frameRate: string): number => {
if (frameRate.includes('/')) {
return frameRate
.split('/')
.map((c) => parseInt(c))
.reduce((p, c) => p / c);
}
return parseInt(frameRate);
};
const persistVolume = (volume: number, muted: boolean) => {
playerSettings.set('volume', volume);
playerSettings.set('muted', muted);
};
const restoreVolume = (video: HTMLVideoElement) => {
const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5;
const muted = (playerSettings.get('muted') as boolean | undefined) ?? false;
video.volume = volume;
video.muted = muted;
};
let hotkeyEventId = 0;
const registerHotkeyEvent = (
setHotkeyEvents: React.Dispatch<React.SetStateAction<HotkeyEvent[]>>,
event: Omit<HotkeyEvent, 'key'>,
) => {
setHotkeyEvents(() => [{ key: hotkeyEventId++, ...event }]);
};
export const Video: React.FC<{
preview?: string;
frameRate?: string;
aspectRatio?: string;
src: string;
alt?: string;
lang?: string;
sensitive?: boolean;
onOpenVideo?: (options: {
startTime: number;
autoPlay: boolean;
defaultVolume: number;
}) => void;
onCloseVideo?: () => void;
detailed?: boolean;
editable?: boolean;
alwaysVisible?: boolean;
visible?: boolean;
onToggleVisibility?: () => void;
deployPictureInPicture?: (
type: string,
mediaProps: {
src: string;
muted: boolean;
volume: number;
currentTime: number;
},
) => void;
blurhash?: string;
startPlaying?: boolean;
startTime?: number;
startVolume?: number;
startMuted?: boolean;
matchedFilters?: string[];
letterbox?: boolean;
fullwidth?: boolean;
}> = ({
preview,
frameRate = '25',
aspectRatio,
src,
alt = '',
lang,
sensitive,
onOpenVideo,
onCloseVideo,
detailed,
editable,
alwaysVisible,
visible,
onToggleVisibility,
deployPictureInPicture,
blurhash,
startPlaying,
startTime,
startVolume,
startMuted,
matchedFilters,
letterbox,
fullwidth,
}) => {
const intl = useIntl();
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(0.5);
const [paused, setPaused] = useState(true);
const [dragging, setDragging] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
const [hovered, setHovered] = useState(false);
const [muted, setMuted] = useState(false);
const [revealed, setRevealed] = useState(false);
const [hotkeyEvents, setHotkeyEvents] = useState<HotkeyEvent[]>([]);
const playerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement | null>(null);
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const doubleClickTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const [style, api] = useSpring(() => ({
progress: '0%',
buffer: '0%',
volume: '0%',
}));
const handleVideoRef = useCallback(
(c: HTMLVideoElement | null) => {
if (videoRef.current && !videoRef.current.paused && c === null) {
deployPictureInPicture?.('video', {
src: src,
currentTime: videoRef.current.currentTime,
muted: videoRef.current.muted,
volume: videoRef.current.volume,
});
}
videoRef.current = c;
if (videoRef.current) {
restoreVolume(videoRef.current);
setVolume(videoRef.current.volume);
setMuted(videoRef.current.muted);
void api.start({
volume: `${videoRef.current.volume * 100}%`,
immediate: reduceMotion,
});
}
},
[api, setVolume, setMuted, src, deployPictureInPicture],
);
const togglePlay = useCallback(() => {
if (!videoRef.current) {
return;
}
if (videoRef.current.paused) {
void videoRef.current.play();
} else {
videoRef.current.pause();
}
}, []);
const toggleFullscreen = useCallback(() => {
if (isFullscreen()) {
exitFullscreen();
} else {
requestFullscreen(playerRef.current);
}
}, []);
const toggleMute = useCallback(() => {
if (!videoRef.current) {
return;
}
const effectivelyMuted =
videoRef.current.muted || videoRef.current.volume === 0;
if (effectivelyMuted) {
videoRef.current.muted = false;
if (videoRef.current.volume === 0) {
videoRef.current.volume = 0.05;
}
} else {
videoRef.current.muted = true;
}
}, []);
const handleClickRoot = useCallback((e: React.MouseEvent) => {
// Stop clicks within the video player e.g. closing parent modal
e.stopPropagation();
}, []);
const handleClick = useCallback(() => {
if (!doubleClickTimeoutRef.current) {
doubleClickTimeoutRef.current = setTimeout(() => {
registerHotkeyEvent(setHotkeyEvents, {
icon: videoRef.current?.paused ? PlayArrowIcon : PauseIcon,
label: videoRef.current?.paused ? messages.play : messages.pause,
});
togglePlay();
doubleClickTimeoutRef.current = null;
}, DOUBLE_CLICK_THRESHOLD);
} else {
clearTimeout(doubleClickTimeoutRef.current);
doubleClickTimeoutRef.current = null;
registerHotkeyEvent(setHotkeyEvents, {
icon: isFullscreen() ? FullscreenExitIcon : FullscreenIcon,
label: isFullscreen() ? messages.exit_fullscreen : messages.fullscreen,
});
toggleFullscreen();
}
}, [setHotkeyEvents, togglePlay, toggleFullscreen]);
const handlePlay = useCallback(() => {
setPaused(false);
}, [setPaused]);
const handlePause = useCallback(() => {
setPaused(true);
}, [setPaused]);
useEffect(() => {
let nextFrame: ReturnType<typeof requestAnimationFrame>;
const updateProgress = () => {
nextFrame = requestAnimationFrame(() => {
if (videoRef.current) {
void api.start({
progress: `${(videoRef.current.currentTime / videoRef.current.duration) * 100}%`,
immediate: reduceMotion,
config: config.stiff,
});
}
updateProgress();
});
};
updateProgress();
return () => {
cancelAnimationFrame(nextFrame);
};
}, [api]);
useEffect(() => {
if (!videoRef.current) {
return;
}
videoRef.current.volume = volume;
videoRef.current.muted = muted;
}, [volume, muted]);
useEffect(() => {
if (typeof visible !== 'undefined') {
setRevealed(visible);
} else {
setRevealed(
displayMedia === 'show_all' ||
(displayMedia !== 'hide_all' && !sensitive),
);
}
}, [visible, sensitive]);
useEffect(() => {
if (!revealed && videoRef.current) {
videoRef.current.pause();
}
}, [revealed]);
useEffect(() => {
const handleFullscreenChange = () => {
setFullscreen(isFullscreen());
};
const handleScroll = throttle(
() => {
if (!videoRef.current) {
return;
}
const { top, height } = videoRef.current.getBoundingClientRect();
const inView =
top <=
(window.innerHeight || document.documentElement.clientHeight) &&
top + height >= 0;
if (!videoRef.current.paused && !inView) {
videoRef.current.pause();
deployPictureInPicture?.('video', {
src: src,
currentTime: videoRef.current.currentTime,
muted: videoRef.current.muted,
volume: videoRef.current.volume,
});
}
},
150,
{ trailing: true },
);
attachFullscreenListener(handleFullscreenChange);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
detachFullscreenListener(handleFullscreenChange);
};
}, [setPaused, setFullscreen, src, deployPictureInPicture]);
const handleTimeUpdate = useCallback(() => {
if (!videoRef.current) {
return;
}
setCurrentTime(videoRef.current.currentTime);
}, [setCurrentTime]);
const handleVolumeMouseDown = useCallback(
(e: React.MouseEvent) => {
const handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', handleVolumeMouseMove, true);
document.removeEventListener('mouseup', handleVolumeMouseUp, true);
};
const handleVolumeMouseMove = (e: MouseEvent) => {
if (!volumeRef.current || !videoRef.current) {
return;
}
const { x } = getPointerPosition(volumeRef.current, e);
if (!isNaN(x)) {
videoRef.current.volume = x;
videoRef.current.muted = x > 0 ? false : true;
void api.start({ volume: `${x * 100}%`, immediate: true });
}
};
document.addEventListener('mousemove', handleVolumeMouseMove, true);
document.addEventListener('mouseup', handleVolumeMouseUp, true);
handleVolumeMouseMove(e.nativeEvent);
e.preventDefault();
e.stopPropagation();
},
[api],
);
const handleSeekMouseDown = useCallback(
(e: React.MouseEvent) => {
const handleSeekMouseUp = () => {
document.removeEventListener('mousemove', handleSeekMouseMove, true);
document.removeEventListener('mouseup', handleSeekMouseUp, true);
setDragging(false);
void videoRef.current?.play();
};
const handleSeekMouseMove = (e: MouseEvent) => {
if (!seekRef.current || !videoRef.current) {
return;
}
const { x } = getPointerPosition(seekRef.current, e);
const newTime = videoRef.current.duration * x;
if (!isNaN(newTime)) {
videoRef.current.currentTime = newTime;
void api.start({ progress: `${x * 100}%`, immediate: true });
}
};
document.addEventListener('mousemove', handleSeekMouseMove, true);
document.addEventListener('mouseup', handleSeekMouseUp, true);
setDragging(true);
videoRef.current?.pause();
handleSeekMouseMove(e.nativeEvent);
e.preventDefault();
e.stopPropagation();
},
[setDragging, api],
);
const seekBy = (time: number) => {
if (!videoRef.current) {
return;
}
const newTime = videoRef.current.currentTime + time;
if (!isNaN(newTime)) {
videoRef.current.currentTime = newTime;
}
};
const updateVolumeBy = (step: number) => {
if (!videoRef.current) {
return;
}
const newVolume = videoRef.current.volume + step;
if (!isNaN(newVolume)) {
videoRef.current.volume = newVolume;
videoRef.current.muted = newVolume > 0 ? false : true;
}
};
const handleVideoKeyDown = useCallback(
(e: React.KeyboardEvent) => {
// On the video element or the seek bar, we can safely use the space bar
// for playback control because there are no buttons to press
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: videoRef.current?.paused ? PlayArrowIcon : PauseIcon,
label: videoRef.current?.paused ? messages.play : messages.pause,
});
togglePlay();
}
},
[setHotkeyEvents, togglePlay],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const frameTime = 1 / frameRateAsNumber(frameRate);
switch (e.key) {
case 'k':
case ' ':
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: videoRef.current?.paused ? PlayArrowIcon : PauseIcon,
label: videoRef.current?.paused ? messages.play : messages.pause,
});
togglePlay();
break;
case 'm':
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: videoRef.current?.muted ? VolumeUpIcon : VolumeOffIcon,
label: videoRef.current?.muted ? messages.unmute : messages.mute,
});
toggleMute();
break;
case 'f':
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: isFullscreen() ? FullscreenExitIcon : FullscreenIcon,
label: isFullscreen()
? messages.exit_fullscreen
: messages.fullscreen,
});
toggleFullscreen();
break;
case 'j':
case 'ArrowLeft':
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: Replay5Icon,
label: messages.skipBackward,
});
seekBy(-5);
break;
case 'l':
case 'ArrowRight':
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: Forward5Icon,
label: messages.skipForward,
});
seekBy(5);
break;
case ',':
e.preventDefault();
e.stopPropagation();
seekBy(-frameTime);
break;
case '.':
e.preventDefault();
e.stopPropagation();
seekBy(frameTime);
break;
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: VolumeUpIcon,
label: messages.volumeUp,
});
updateVolumeBy(0.15);
break;
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
registerHotkeyEvent(setHotkeyEvents, {
icon: VolumeDownIcon,
label: messages.volumeDown,
});
updateVolumeBy(-0.15);
break;
}
// If we are in fullscreen mode, we don't want any hotkeys
// interacting with the UI that's not visible
if (fullscreen) {
e.preventDefault();
e.stopPropagation();
if (e.key === 'Escape') {
setHotkeyEvents((events) => [
...events,
{
key: hotkeyEventId++,
icon: FullscreenExitIcon,
label: messages.exit_fullscreen,
},
]);
exitFullscreen();
}
}
},
[
setHotkeyEvents,
togglePlay,
toggleFullscreen,
toggleMute,
fullscreen,
frameRate,
],
);
const handleMouseEnter = useCallback(() => {
setHovered(true);
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
hoverTimeoutRef.current = setTimeout(() => {
setHovered(false);
}, HOVER_FADE_DELAY);
}, [setHovered]);
const handleMouseMove = useCallback(() => {
setHovered(true);
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
hoverTimeoutRef.current = setTimeout(() => {
setHovered(false);
}, HOVER_FADE_DELAY);
}, [setHovered]);
const handleMouseLeave = useCallback(() => {
setHovered(false);
}, [setHovered]);
const toggleReveal = useCallback(() => {
if (onToggleVisibility) {
onToggleVisibility();
} else {
setRevealed((value) => !value);
}
}, [setRevealed, onToggleVisibility]);
const handleLoadedData = useCallback(() => {
if (!videoRef.current) {
return;
}
setDuration(videoRef.current.duration);
if (typeof startTime !== 'undefined') {
videoRef.current.currentTime = startTime;
}
if (typeof startVolume !== 'undefined') {
videoRef.current.volume = startVolume;
}
if (typeof startMuted !== 'undefined') {
videoRef.current.muted = startMuted;
}
if (startPlaying) {
void videoRef.current.play();
}
}, [setDuration, startTime, startVolume, startMuted, startPlaying]);
const handleProgress = useCallback(() => {
if (!videoRef.current) {
return;
}
const lastTimeRange = videoRef.current.buffered.length - 1;
if (lastTimeRange > -1) {
void api.start({
buffer: `${Math.ceil(videoRef.current.buffered.end(lastTimeRange) / videoRef.current.duration) * 100}%`,
immediate: reduceMotion,
});
}
}, [api]);
const handleVolumeChange = useCallback(() => {
if (!videoRef.current) {
return;
}
setVolume(videoRef.current.volume);
setMuted(videoRef.current.muted);
void api.start({
volume: `${videoRef.current.muted ? 0 : videoRef.current.volume * 100}%`,
immediate: reduceMotion,
});
persistVolume(videoRef.current.volume, videoRef.current.muted);
}, [api, setVolume, setMuted]);
const handleOpenVideo = useCallback(() => {
if (!videoRef.current) {
return;
}
const wasPaused = videoRef.current.paused;
videoRef.current.pause();
onOpenVideo?.({
startTime: videoRef.current.currentTime,
autoPlay: !wasPaused,
defaultVolume: videoRef.current.volume,
});
}, [onOpenVideo]);
const handleCloseVideo = useCallback(() => {
if (!videoRef.current) {
return;
}
videoRef.current.pause();
onCloseVideo?.();
}, [onCloseVideo]);
const handleHotkeyEventDismiss = useCallback(
({ key }: HotkeyEvent) => {
setHotkeyEvents((events) => events.filter((e) => e.key !== key));
},
[setHotkeyEvents],
);
const progress = Math.min((currentTime / duration) * 100, 100);
const effectivelyMuted = muted || volume === 0;
let preload;
if (startTime || fullscreen || dragging) {
preload = 'auto';
} else if (detailed) {
preload = 'metadata';
} else {
preload = 'none';
}
// The outer wrapper is necessary to avoid reflowing the layout when going into full screen
return (
<div>
<div
role='menuitem'
className={classNames('video-player', {
inactive: !revealed,
detailed,
fullscreen,
editable,
letterbox,
'full-width': fullwidth,
})}
style={{ aspectRatio }}
ref={playerRef}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClickRoot}
onKeyDown={handleKeyDown}
tabIndex={0}
>
{blurhash && (
<Blurhash
hash={blurhash}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': revealed,
})}
dummy={!useBlurhash}
/>
)}
{(revealed || editable) && (
<video /* eslint-disable-line jsx-a11y/media-has-caption */
ref={handleVideoRef}
src={src}
poster={preview}
preload={preload}
role='button'
tabIndex={0}
aria-label={alt}
title={alt}
lang={lang}
onClick={handleClick}
onKeyDown={handleVideoKeyDown}
onPlay={handlePlay}
onPause={handlePause}
onLoadedData={handleLoadedData}
onProgress={handleProgress}
onTimeUpdate={handleTimeUpdate}
onVolumeChange={handleVolumeChange}
style={{ width: '100%' }}
/>
)}
<HotkeyIndicator
events={hotkeyEvents}
onDismiss={handleHotkeyEventDismiss}
/>
<SpoilerButton
hidden={revealed || editable}
sensitive={sensitive ?? false}
onClick={toggleReveal}
matchedFilters={matchedFilters}
/>
{!onCloseVideo &&
!editable &&
!fullscreen &&
!alwaysVisible &&
revealed && (
<div
className={classNames('media-gallery__actions', {
active: paused || hovered,
})}
>
<button
className='media-gallery__actions__pill'
onClick={toggleReveal}
>
<FormattedMessage
id='media_gallery.hide'
defaultMessage='Hide'
/>
</button>
</div>
)}
<div
className={classNames('video-player__controls', {
active: paused || hovered,
})}
>
<div
className='video-player__seek'
role='slider'
aria-valuemin={0}
aria-valuenow={progress}
aria-valuemax={100}
onMouseDown={handleSeekMouseDown}
onKeyDown={handleVideoKeyDown}
tabIndex={0}
ref={seekRef}
>
<animated.div
className='video-player__seek__buffer'
style={{ width: style.buffer }}
/>
<animated.div
className='video-player__seek__progress'
style={{ width: style.progress }}
/>
<animated.span
className={classNames('video-player__seek__handle', {
active: dragging,
})}
style={{ left: style.progress }}
/>
</div>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button
type='button'
title={intl.formatMessage(
paused ? messages.play : messages.pause,
)}
aria-label={intl.formatMessage(
paused ? messages.play : messages.pause,
)}
className='player-button'
onClick={togglePlay}
>
<Icon
id={paused ? 'play' : 'pause'}
icon={paused ? PlayArrowIcon : PauseIcon}
/>
</button>
<button
type='button'
title={intl.formatMessage(
effectivelyMuted ? messages.unmute : messages.mute,
)}
aria-label={intl.formatMessage(
muted ? messages.unmute : messages.mute,
)}
className='player-button'
onClick={toggleMute}
>
<Icon
id={effectivelyMuted ? 'volume-off' : 'volume-up'}
icon={effectivelyMuted ? VolumeOffIcon : VolumeUpIcon}
/>
</button>
<div
className={classNames('video-player__volume', {
active: hovered,
})}
role='slider'
aria-valuemin={0}
aria-valuenow={effectivelyMuted ? 0 : volume * 100}
aria-valuemax={100}
onMouseDown={handleVolumeMouseDown}
ref={volumeRef}
tabIndex={0}
>
<animated.div
className='video-player__volume__current'
style={{ width: style.volume }}
/>
<animated.span
className={classNames('video-player__volume__handle')}
style={{ left: style.volume }}
/>
</div>
{(detailed || fullscreen) && (
<span className='video-player__time'>
<span className='video-player__time-current'>
{formatTime(Math.floor(currentTime))}
</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>
{formatTime(Math.floor(duration))}
</span>
</span>
)}
</div>
<div className='video-player__buttons right'>
{!fullscreen && onOpenVideo && (
<button
type='button'
title={intl.formatMessage(messages.expand)}
aria-label={intl.formatMessage(messages.expand)}
className='player-button'
onClick={handleOpenVideo}
>
<Icon id='expand' icon={RectangleIcon} />
</button>
)}
{onCloseVideo && (
<button
type='button'
title={intl.formatMessage(messages.close)}
aria-label={intl.formatMessage(messages.close)}
className='player-button'
onClick={handleCloseVideo}
>
<Icon id='compress' icon={FullscreenExitIcon} />
</button>
)}
<button
type='button'
title={intl.formatMessage(
fullscreen ? messages.exit_fullscreen : messages.fullscreen,
)}
aria-label={intl.formatMessage(
fullscreen ? messages.exit_fullscreen : messages.fullscreen,
)}
className='player-button'
onClick={toggleFullscreen}
>
<Icon
id={fullscreen ? 'compress' : 'arrows-alt'}
icon={fullscreen ? FullscreenExitIcon : FullscreenIcon}
/>
</button>
</div>
</div>
</div>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default Video;