Media Viewer: Better display for time ranges (#1779)

This commit is contained in:
Alexander Zinchuk 2022-03-19 21:19:59 +01:00
parent 9ceea488f9
commit 1c50550079
6 changed files with 63 additions and 25 deletions

View File

@ -233,7 +233,6 @@
} }
} }
.seekline-buffered-progress,
.seekline-play-progress { .seekline-play-progress {
position: absolute; position: absolute;
height: 2px; height: 2px;
@ -252,8 +251,14 @@
} }
} }
.seekline-buffered-progress i { .seekline-buffered-progress {
background-color: var(--color-interactive-buffered) !important; height: 2px;
border-radius: 2px;
position: absolute;
top: 6px;
background-color: var(--color-interactive-buffered);
} }
.seekline-thumb { .seekline-thumb {
@ -352,6 +357,9 @@
} }
} }
.has-replies .Audio[dir="rtl"] { .has-replies .Audio {
margin-bottom: 1.625rem; margin-bottom: 1rem;
[dir="rtl"] {
margin-bottom: 1.625rem;
}
} }

View File

@ -25,7 +25,7 @@ import { getFileSizeString } from './helpers/documentInfo';
import { decodeWaveform, interpolateArray } from '../../util/waveform'; import { decodeWaveform, interpolateArray } from '../../util/waveform';
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress'; import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useBuffering from '../../hooks/useBuffering'; import useBuffering, { BufferedRange } from '../../hooks/useBuffering';
import useAudioPlayer from '../../hooks/useAudioPlayer'; import useAudioPlayer from '../../hooks/useAudioPlayer';
import useLang, { LangFn } from '../../hooks/useLang'; import useLang, { LangFn } from '../../hooks/useLang';
import { captureEvents } from '../../util/captureEvents'; import { captureEvents } from '../../util/captureEvents';
@ -116,7 +116,7 @@ const Audio: FC<OwnProps> = ({
}, []); }, []);
const { const {
isBuffered, bufferedProgress, bufferingHandlers, checkBuffering, isBuffered, bufferedRanges, bufferingHandlers, checkBuffering,
} = useBuffering(); } = useBuffering();
const { const {
@ -296,7 +296,7 @@ const Audio: FC<OwnProps> = ({
<span className="duration with-seekline" dir="auto"> <span className="duration with-seekline" dir="auto">
{playProgress < 1 && `${formatMediaDuration(duration * playProgress, duration)}`} {playProgress < 1 && `${formatMediaDuration(duration * playProgress, duration)}`}
</span> </span>
{renderSeekline(playProgress, bufferedProgress, seekerRef)} {renderSeekline(playProgress, bufferedRanges, seekerRef)}
</div> </div>
)} )}
{!withSeekline && renderSecondLine()} {!withSeekline && renderSecondLine()}
@ -354,7 +354,7 @@ const Audio: FC<OwnProps> = ({
duration, duration,
isPlaying, isPlaying,
playProgress, playProgress,
bufferedProgress, bufferedRanges,
seekerRef, seekerRef,
(isDownloading || isUploading), (isDownloading || isUploading),
date, date,
@ -375,7 +375,7 @@ function renderAudio(
duration: number, duration: number,
isPlaying: boolean, isPlaying: boolean,
playProgress: number, playProgress: number,
bufferedProgress: number, bufferedRanges: BufferedRange[],
seekerRef: React.Ref<HTMLElement>, seekerRef: React.Ref<HTMLElement>,
showProgress?: boolean, showProgress?: boolean,
date?: number, date?: number,
@ -396,7 +396,7 @@ function renderAudio(
<span className="duration with-seekline" dir="auto"> <span className="duration with-seekline" dir="auto">
{formatMediaDuration(duration * playProgress, duration)} {formatMediaDuration(duration * playProgress, duration)}
</span> </span>
{renderSeekline(playProgress, bufferedProgress, seekerRef)} {renderSeekline(playProgress, bufferedRanges, seekerRef)}
</div> </div>
)} )}
{!showSeekline && showProgress && ( {!showSeekline && showProgress && (
@ -499,7 +499,7 @@ function useWaveformCanvas(
function renderSeekline( function renderSeekline(
playProgress: number, playProgress: number,
bufferedProgress: number, bufferedRanges: BufferedRange[],
seekerRef: React.Ref<HTMLElement>, seekerRef: React.Ref<HTMLElement>,
) { ) {
return ( return (
@ -507,11 +507,12 @@ function renderSeekline(
className="seekline no-selection" className="seekline no-selection"
ref={seekerRef as React.Ref<HTMLDivElement>} ref={seekerRef as React.Ref<HTMLDivElement>}
> >
<span className="seekline-buffered-progress"> {bufferedRanges.map(({ start, end }) => (
<i <div
style={`transform: translateX(${bufferedProgress * 100}%)`} className="seekline-buffered-progress"
style={`left: ${start * 100}%; right: ${100 - end * 100}%`}
/> />
</span> ))}
<span className="seekline-play-progress"> <span className="seekline-play-progress">
<i <i
style={`transform: translateX(${playProgress * 100}%)`} style={`transform: translateX(${playProgress * 100}%)`}

View File

@ -66,7 +66,9 @@ const VideoPlayer: FC<OwnProps> = ({
const [isFullscreen, setFullscreen, exitFullscreen] = useFullscreenStatus(videoRef, setIsPlayed); const [isFullscreen, setFullscreen, exitFullscreen] = useFullscreenStatus(videoRef, setIsPlayed);
const { isBuffered, bufferedProgress, bufferingHandlers } = useBuffering(); const {
isBuffered, bufferedRanges, bufferingHandlers, bufferedProgress,
} = useBuffering();
const { const {
shouldRender: shouldRenderSpinner, shouldRender: shouldRenderSpinner,
transitionClassNames: spinnerClassNames, transitionClassNames: spinnerClassNames,
@ -229,6 +231,7 @@ const VideoPlayer: FC<OwnProps> = ({
{!isGif && !shouldRenderSpinner && ( {!isGif && !shouldRenderSpinner && (
<VideoPlayerControls <VideoPlayerControls
isPlayed={isPlayed} isPlayed={isPlayed}
bufferedRanges={bufferedRanges}
bufferedProgress={bufferedProgress} bufferedProgress={bufferedProgress}
isBuffered={isBuffered} isBuffered={isBuffered}
currentTime={currentTime} currentTime={currentTime}

View File

@ -126,6 +126,7 @@
} }
&-buffered { &-buffered {
position: absolute;
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
} }

View File

@ -8,6 +8,7 @@ import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { formatMediaDuration } from '../../util/dateFormat'; import { formatMediaDuration } from '../../util/dateFormat';
import formatFileSize from './helpers/formatFileSize'; import formatFileSize from './helpers/formatFileSize';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { BufferedRange } from '../../hooks/useBuffering';
import { captureEvents } from '../../util/captureEvents'; import { captureEvents } from '../../util/captureEvents';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -18,6 +19,7 @@ import MenuItem from '../ui/MenuItem';
import './VideoPlayerControls.scss'; import './VideoPlayerControls.scss';
type OwnProps = { type OwnProps = {
bufferedRanges: BufferedRange[];
bufferedProgress: number; bufferedProgress: number;
currentTime: number; currentTime: number;
duration: number; duration: number;
@ -54,6 +56,7 @@ const PLAYBACK_RATES = [
const HIDE_CONTROLS_TIMEOUT_MS = 1500; const HIDE_CONTROLS_TIMEOUT_MS = 1500;
const VideoPlayerControls: FC<OwnProps> = ({ const VideoPlayerControls: FC<OwnProps> = ({
bufferedRanges,
bufferedProgress, bufferedProgress,
currentTime, currentTime,
duration, duration,
@ -156,7 +159,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isVisible && 'active')} className={buildClassName('VideoPlayerControls', isForceMobileVersion && 'mobile', isVisible && 'active')}
onClick={stopEvent} onClick={stopEvent}
> >
{renderSeekLine(currentTime, duration, bufferedProgress, seekerRef)} {renderSeekLine(currentTime, duration, bufferedRanges, seekerRef)}
<div className="buttons"> <div className="buttons">
<Button <Button
ariaLabel={lang('AccActionPlay')} ariaLabel={lang('AccActionPlay')}
@ -243,18 +246,19 @@ function renderFileSize(loadedPercent: number, totalSize: number) {
} }
function renderSeekLine( function renderSeekLine(
currentTime: number, duration: number, bufferedProgress: number, seekerRef: React.RefObject<HTMLDivElement>, currentTime: number, duration: number, bufferedRanges: BufferedRange[], seekerRef: React.RefObject<HTMLDivElement>,
) { ) {
const percentagePlayed = (currentTime / duration) * 100; const percentagePlayed = (currentTime / duration) * 100;
const percentageBuffered = bufferedProgress * 100;
return ( return (
<div className="player-seekline" ref={seekerRef}> <div className="player-seekline" ref={seekerRef}>
<div className="player-seekline-track"> <div className="player-seekline-track">
<div {bufferedRanges.map(({ start, end }) => (
className="player-seekline-buffered" <div
style={`width: ${percentageBuffered || 0}%`} className="player-seekline-buffered"
/> style={`left: ${start * 100}%; right: ${100 - end * 100}%`}
/>
))}
<div <div
className="player-seekline-played" className="player-seekline-played"
style={`width: ${percentagePlayed || 0}%`} style={`width: ${percentagePlayed || 0}%`}

View File

@ -8,9 +8,15 @@ const MIN_READY_STATE = 3;
// Avoid flickering when re-mounting previously buffered video // Avoid flickering when re-mounting previously buffered video
const DEBOUNCE = 200; const DEBOUNCE = 200;
/**
* Time range relative to the duration [0, 1]
*/
export type BufferedRange = { start: number; end: number };
const useBuffering = (noInitiallyBuffered = false) => { const useBuffering = (noInitiallyBuffered = false) => {
const [isBuffered, setIsBuffered] = useState(!noInitiallyBuffered); const [isBuffered, setIsBuffered] = useState(!noInitiallyBuffered);
const [bufferedProgress, setBufferedProgress] = useState(0); const [bufferedProgress, setBufferedProgress] = useState(0);
const [bufferedRanges, setBufferedRanges] = useState<BufferedRange[]>([]);
const setIsBufferedDebounced = useMemo(() => { const setIsBufferedDebounced = useMemo(() => {
return debounce(setIsBuffered, DEBOUNCE, false, true); return debounce(setIsBuffered, DEBOUNCE, false, true);
@ -21,7 +27,10 @@ const useBuffering = (noInitiallyBuffered = false) => {
if (!isSafariPatchInProgress(media)) { if (!isSafariPatchInProgress(media)) {
if (media.buffered.length) { if (media.buffered.length) {
setBufferedProgress(media.buffered.end(0) / media.duration); const ranges = getTimeRanges(media.buffered, media.duration);
setBufferedRanges(ranges);
const bufferedLength = ranges.reduce((acc, { start, end }) => acc + end - start, 0);
setBufferedProgress(bufferedLength / media.duration);
} }
setIsBufferedDebounced(media.readyState >= MIN_READY_STATE || media.currentTime > 0); setIsBufferedDebounced(media.readyState >= MIN_READY_STATE || media.currentTime > 0);
@ -40,6 +49,7 @@ const useBuffering = (noInitiallyBuffered = false) => {
return { return {
isBuffered, isBuffered,
bufferedProgress, bufferedProgress,
bufferedRanges,
bufferingHandlers, bufferingHandlers,
checkBuffering(element: HTMLMediaElement) { checkBuffering(element: HTMLMediaElement) {
setIsBufferedDebounced(element.readyState >= MIN_READY_STATE); setIsBufferedDebounced(element.readyState >= MIN_READY_STATE);
@ -47,4 +57,15 @@ const useBuffering = (noInitiallyBuffered = false) => {
}; };
}; };
function getTimeRanges(ranges: TimeRanges, duration: number) {
const result: BufferedRange[] = [];
for (let i = 0; i < ranges.length; i++) {
result.push({
start: ranges.start(i) / duration,
end: ranges.end(i) / duration,
});
}
return result;
}
export default useBuffering; export default useBuffering;