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 {
position: absolute;
height: 2px;
@ -252,8 +251,14 @@
}
}
.seekline-buffered-progress i {
background-color: var(--color-interactive-buffered) !important;
.seekline-buffered-progress {
height: 2px;
border-radius: 2px;
position: absolute;
top: 6px;
background-color: var(--color-interactive-buffered);
}
.seekline-thumb {
@ -352,6 +357,9 @@
}
}
.has-replies .Audio[dir="rtl"] {
margin-bottom: 1.625rem;
.has-replies .Audio {
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 useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
import useShowTransition from '../../hooks/useShowTransition';
import useBuffering from '../../hooks/useBuffering';
import useBuffering, { BufferedRange } from '../../hooks/useBuffering';
import useAudioPlayer from '../../hooks/useAudioPlayer';
import useLang, { LangFn } from '../../hooks/useLang';
import { captureEvents } from '../../util/captureEvents';
@ -116,7 +116,7 @@ const Audio: FC<OwnProps> = ({
}, []);
const {
isBuffered, bufferedProgress, bufferingHandlers, checkBuffering,
isBuffered, bufferedRanges, bufferingHandlers, checkBuffering,
} = useBuffering();
const {
@ -296,7 +296,7 @@ const Audio: FC<OwnProps> = ({
<span className="duration with-seekline" dir="auto">
{playProgress < 1 && `${formatMediaDuration(duration * playProgress, duration)}`}
</span>
{renderSeekline(playProgress, bufferedProgress, seekerRef)}
{renderSeekline(playProgress, bufferedRanges, seekerRef)}
</div>
)}
{!withSeekline && renderSecondLine()}
@ -354,7 +354,7 @@ const Audio: FC<OwnProps> = ({
duration,
isPlaying,
playProgress,
bufferedProgress,
bufferedRanges,
seekerRef,
(isDownloading || isUploading),
date,
@ -375,7 +375,7 @@ function renderAudio(
duration: number,
isPlaying: boolean,
playProgress: number,
bufferedProgress: number,
bufferedRanges: BufferedRange[],
seekerRef: React.Ref<HTMLElement>,
showProgress?: boolean,
date?: number,
@ -396,7 +396,7 @@ function renderAudio(
<span className="duration with-seekline" dir="auto">
{formatMediaDuration(duration * playProgress, duration)}
</span>
{renderSeekline(playProgress, bufferedProgress, seekerRef)}
{renderSeekline(playProgress, bufferedRanges, seekerRef)}
</div>
)}
{!showSeekline && showProgress && (
@ -499,7 +499,7 @@ function useWaveformCanvas(
function renderSeekline(
playProgress: number,
bufferedProgress: number,
bufferedRanges: BufferedRange[],
seekerRef: React.Ref<HTMLElement>,
) {
return (
@ -507,11 +507,12 @@ function renderSeekline(
className="seekline no-selection"
ref={seekerRef as React.Ref<HTMLDivElement>}
>
<span className="seekline-buffered-progress">
<i
style={`transform: translateX(${bufferedProgress * 100}%)`}
{bufferedRanges.map(({ start, end }) => (
<div
className="seekline-buffered-progress"
style={`left: ${start * 100}%; right: ${100 - end * 100}%`}
/>
</span>
))}
<span className="seekline-play-progress">
<i
style={`transform: translateX(${playProgress * 100}%)`}

View File

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

View File

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

View File

@ -8,9 +8,15 @@ const MIN_READY_STATE = 3;
// Avoid flickering when re-mounting previously buffered video
const DEBOUNCE = 200;
/**
* Time range relative to the duration [0, 1]
*/
export type BufferedRange = { start: number; end: number };
const useBuffering = (noInitiallyBuffered = false) => {
const [isBuffered, setIsBuffered] = useState(!noInitiallyBuffered);
const [bufferedProgress, setBufferedProgress] = useState(0);
const [bufferedRanges, setBufferedRanges] = useState<BufferedRange[]>([]);
const setIsBufferedDebounced = useMemo(() => {
return debounce(setIsBuffered, DEBOUNCE, false, true);
@ -21,7 +27,10 @@ const useBuffering = (noInitiallyBuffered = false) => {
if (!isSafariPatchInProgress(media)) {
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);
@ -40,6 +49,7 @@ const useBuffering = (noInitiallyBuffered = false) => {
return {
isBuffered,
bufferedProgress,
bufferedRanges,
bufferingHandlers,
checkBuffering(element: HTMLMediaElement) {
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;