[Perf] Audio: Get rid of slow canvas.toDataURL()

This commit is contained in:
Alexander Zinchuk 2021-11-05 21:58:11 +03:00
parent d6e1665e4e
commit 3d3a488622
2 changed files with 68 additions and 63 deletions

View File

@ -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(

View File

@ -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(