[Perf] Audio: Get rid of slow canvas.toDataURL()
This commit is contained in:
parent
d6e1665e4e
commit
3d3a488622
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useEffect, useMemo, useRef, useState,
|
FC, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getDispatch } from '../../lib/teact/teactn';
|
import { getDispatch } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ import {
|
|||||||
isMessageLocal,
|
isMessageLocal,
|
||||||
isOwnMessage,
|
isOwnMessage,
|
||||||
} from '../../modules/helpers';
|
} from '../../modules/helpers';
|
||||||
import { renderWaveformToDataUri } from './helpers/waveform';
|
import { renderWaveform } from './helpers/waveform';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { getFileSizeString } from './helpers/documentInfo';
|
import { getFileSizeString } from './helpers/documentInfo';
|
||||||
@ -85,7 +85,7 @@ const Audio: FC<OwnProps> = ({
|
|||||||
const isSeeking = useRef<boolean>(false);
|
const isSeeking = useRef<boolean>(false);
|
||||||
const playStateBeforeSeeking = useRef<boolean>(false);
|
const playStateBeforeSeeking = useRef<boolean>(false);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const seekerRef = useRef<HTMLElement>(null);
|
const seekerRef = useRef<HTMLDivElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const { isRtl } = lang;
|
const { isRtl } = lang;
|
||||||
const dispatch = getDispatch();
|
const dispatch = getDispatch();
|
||||||
@ -136,6 +136,9 @@ const Audio: FC<OwnProps> = ({
|
|||||||
isMessageLocal(message),
|
isMessageLocal(message),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isOwn = isOwnMessage(message);
|
||||||
|
const waveformCanvasRef = useWaveformCanvas(theme, voice, (isMediaUnread && !isOwn) ? 1 : playProgress, isOwn);
|
||||||
|
|
||||||
const withSeekline = isPlaying || (playProgress > 0 && playProgress < 1);
|
const withSeekline = isPlaying || (playProgress > 0 && playProgress < 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -224,7 +227,7 @@ const Audio: FC<OwnProps> = ({
|
|||||||
});
|
});
|
||||||
}, [withSeekline, handleStartSeek, handleSeek, handleStopSeek]);
|
}, [withSeekline, handleStartSeek, handleSeek, handleStopSeek]);
|
||||||
|
|
||||||
function getFirstLine() {
|
function renderFirstLine() {
|
||||||
if (isVoice) {
|
if (isVoice) {
|
||||||
return senderTitle || 'Voice';
|
return senderTitle || 'Voice';
|
||||||
}
|
}
|
||||||
@ -234,7 +237,7 @@ const Audio: FC<OwnProps> = ({
|
|||||||
return title || fileName;
|
return title || fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSecondLine() {
|
function renderSecondLine() {
|
||||||
if (isVoice) {
|
if (isVoice) {
|
||||||
return (
|
return (
|
||||||
<div className="meta" dir={isRtl ? 'rtl' : undefined}>
|
<div className="meta" dir={isRtl ? 'rtl' : undefined}>
|
||||||
@ -256,18 +259,6 @@ const Audio: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOwn = isOwnMessage(message);
|
|
||||||
const renderedWaveform = useMemo(
|
|
||||||
() => origin === AudioOrigin.Inline && voice && renderWaveform(
|
|
||||||
voice,
|
|
||||||
(isMediaUnread && !isOwn) ? 1 : playProgress,
|
|
||||||
isOwn,
|
|
||||||
theme,
|
|
||||||
seekerRef,
|
|
||||||
),
|
|
||||||
[origin, voice, isMediaUnread, isOwn, playProgress, theme],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'Audio',
|
'Audio',
|
||||||
className,
|
className,
|
||||||
@ -292,7 +283,7 @@ const Audio: FC<OwnProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div className={contentClassName}>
|
<div className={contentClassName}>
|
||||||
<div className="content-row">
|
<div className="content-row">
|
||||||
<p className="title" dir="auto" title={getFirstLine()}>{renderText(getFirstLine())}</p>
|
<p className="title" dir="auto" title={renderFirstLine()}>{renderText(renderFirstLine())}</p>
|
||||||
|
|
||||||
<div className="message-date">
|
<div className="message-date">
|
||||||
{date && (
|
{date && (
|
||||||
@ -314,7 +305,7 @@ const Audio: FC<OwnProps> = ({
|
|||||||
{renderSeekline(playProgress, bufferedProgress, seekerRef)}
|
{renderSeekline(playProgress, bufferedProgress, seekerRef)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!withSeekline && getSecondLine()}
|
{!withSeekline && renderSecondLine()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -369,7 +360,9 @@ const Audio: FC<OwnProps> = ({
|
|||||||
(isDownloading || isUploading), date, transferProgress, onDateClick ? handleDateClick : undefined,
|
(isDownloading || isUploading), date, transferProgress, onDateClick ? handleDateClick : undefined,
|
||||||
)}
|
)}
|
||||||
{origin === AudioOrigin.SharedMedia && (voice || video) && renderWithTitle()}
|
{origin === AudioOrigin.SharedMedia && (voice || video) && renderWithTitle()}
|
||||||
{origin === AudioOrigin.Inline && voice && renderVoice(voice, renderedWaveform, playProgress, isMediaUnread)}
|
{origin === AudioOrigin.Inline && voice && (
|
||||||
|
renderVoice(voice, seekerRef, waveformCanvasRef, playProgress, isMediaUnread)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -428,10 +421,22 @@ function renderAudio(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVoice(voice: ApiVoice, renderedWaveform: any, playProgress: number, isMediaUnread?: boolean) {
|
function renderVoice(
|
||||||
|
voice: ApiVoice,
|
||||||
|
seekerRef: React.Ref<HTMLDivElement>,
|
||||||
|
waveformCanvasRef: React.Ref<HTMLCanvasElement>,
|
||||||
|
playProgress: number,
|
||||||
|
isMediaUnread?: boolean,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="content">
|
<div className="content">
|
||||||
{renderedWaveform}
|
<div
|
||||||
|
className="waveform"
|
||||||
|
draggable={false}
|
||||||
|
ref={seekerRef}
|
||||||
|
>
|
||||||
|
<canvas ref={waveformCanvasRef} />
|
||||||
|
</div>
|
||||||
<p className={buildClassName('voice-duration', isMediaUnread && 'unread')} dir="auto">
|
<p className={buildClassName('voice-duration', isMediaUnread && 'unread')} dir="auto">
|
||||||
{playProgress === 0 ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}
|
{playProgress === 0 ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}
|
||||||
</p>
|
</p>
|
||||||
@ -439,45 +444,51 @@ function renderVoice(voice: ApiVoice, renderedWaveform: any, playProgress: numbe
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWaveform(
|
function useWaveformCanvas(
|
||||||
voice: ApiVoice,
|
theme: ISettings['theme'],
|
||||||
|
voice?: ApiVoice,
|
||||||
playProgress = 0,
|
playProgress = 0,
|
||||||
isOwn = false,
|
isOwn = false,
|
||||||
theme: ISettings['theme'],
|
|
||||||
seekerRef: React.Ref<HTMLElement>,
|
|
||||||
) {
|
) {
|
||||||
const { waveform, duration } = voice;
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
if (!waveform) {
|
const { data: spikes, peak } = useMemo(() => {
|
||||||
return undefined;
|
if (!voice) {
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const fillColor = theme === 'dark' ? '#494A78' : '#ADD3F7';
|
const { waveform, duration } = voice;
|
||||||
const fillOwnColor = theme === 'dark' ? '#B7ABED' : '#AEDFA4';
|
if (!waveform) {
|
||||||
const progressFillColor = theme === 'dark' ? '#8774E1' : '#3390EC';
|
return undefined;
|
||||||
const progressFillOwnColor = theme === 'dark' ? '#FFFFFF' : '#4FAE4E';
|
}
|
||||||
const durationFactor = Math.min(duration / AVG_VOICE_DURATION, 1);
|
|
||||||
const spikesCount = Math.round(MIN_SPIKES + (MAX_SPIKES - MIN_SPIKES) * durationFactor);
|
|
||||||
const decodedWaveform = decodeWaveform(new Uint8Array(waveform));
|
|
||||||
const { data: spikes, peak } = interpolateArray(decodedWaveform, spikesCount);
|
|
||||||
const { src, width, height } = renderWaveformToDataUri(spikes, playProgress, {
|
|
||||||
peak,
|
|
||||||
fillStyle: isOwn ? fillOwnColor : fillColor,
|
|
||||||
progressFillStyle: isOwn ? progressFillOwnColor : progressFillColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
const durationFactor = Math.min(duration / AVG_VOICE_DURATION, 1);
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
const spikesCount = Math.round(MIN_SPIKES + (MAX_SPIKES - MIN_SPIKES) * durationFactor);
|
||||||
<img
|
const decodedWaveform = decodeWaveform(new Uint8Array(waveform));
|
||||||
src={src}
|
|
||||||
alt=""
|
return interpolateArray(decodedWaveform, spikesCount);
|
||||||
width={width}
|
}, [voice]) || {};
|
||||||
height={height}
|
|
||||||
className="waveform"
|
useLayoutEffect(() => {
|
||||||
draggable={false}
|
const canvas = canvasRef.current;
|
||||||
ref={seekerRef as React.Ref<HTMLImageElement>}
|
if (!canvas || !spikes || !peak) {
|
||||||
/>
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
const fillColor = theme === 'dark' ? '#494A78' : '#ADD3F7';
|
||||||
|
const fillOwnColor = theme === 'dark' ? '#B7ABED' : '#AEDFA4';
|
||||||
|
const progressFillColor = theme === 'dark' ? '#8774E1' : '#3390EC';
|
||||||
|
const progressFillOwnColor = theme === 'dark' ? '#FFFFFF' : '#4FAE4E';
|
||||||
|
|
||||||
|
renderWaveform(canvas, spikes, playProgress, {
|
||||||
|
peak,
|
||||||
|
fillStyle: isOwn ? fillOwnColor : fillColor,
|
||||||
|
progressFillStyle: isOwn ? progressFillOwnColor : progressFillColor,
|
||||||
|
});
|
||||||
|
}, [isOwn, peak, playProgress, spikes, theme]);
|
||||||
|
|
||||||
|
return canvasRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSeekline(
|
function renderSeekline(
|
||||||
|
|||||||
@ -9,7 +9,8 @@ const SPIKE_STEP = 4;
|
|||||||
const SPIKE_RADIUS = 1;
|
const SPIKE_RADIUS = 1;
|
||||||
const HEIGHT = 23;
|
const HEIGHT = 23;
|
||||||
|
|
||||||
export function renderWaveformToDataUri(
|
export function renderWaveform(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
spikes: number[],
|
spikes: number[],
|
||||||
progress: number,
|
progress: number,
|
||||||
{
|
{
|
||||||
@ -19,7 +20,6 @@ export function renderWaveformToDataUri(
|
|||||||
const width = spikes.length * SPIKE_STEP;
|
const width = spikes.length * SPIKE_STEP;
|
||||||
const height = HEIGHT;
|
const height = HEIGHT;
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = width * 2;
|
canvas.width = width * 2;
|
||||||
canvas.height = height * 2;
|
canvas.height = height * 2;
|
||||||
canvas.style.width = `${width}px`;
|
canvas.style.width = `${width}px`;
|
||||||
@ -35,12 +35,6 @@ export function renderWaveformToDataUri(
|
|||||||
roundedRectangle(ctx, i * SPIKE_STEP, height, SPIKE_WIDTH, spikeHeight, SPIKE_RADIUS);
|
roundedRectangle(ctx, i * SPIKE_STEP, height, SPIKE_WIDTH, spikeHeight, SPIKE_RADIUS);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
src: canvas.toDataURL(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundedRectangle(
|
function roundedRectangle(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user