mirror of
https://github.com/danog/telegram-tt.git
synced 2024-11-27 04:45:08 +01:00
Media Viewer: Better display for time ranges (#1779)
This commit is contained in:
parent
9ceea488f9
commit
1c50550079
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}%)`}
|
||||||
|
@ -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}
|
||||||
|
@ -126,6 +126,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-buffered {
|
&-buffered {
|
||||||
|
position: absolute;
|
||||||
background-color: rgba(255, 255, 255, 0.5);
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}%`}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user