From 3d3a488622697af07fefa32e3049eb0c6eac73c4 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 5 Nov 2021 21:58:11 +0300 Subject: [PATCH] [Perf] Audio: Get rid of slow `canvas.toDataURL()` --- src/components/common/Audio.tsx | 121 ++++++++++++---------- src/components/common/helpers/waveform.ts | 10 +- 2 files changed, 68 insertions(+), 63 deletions(-) diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index bdb342d60..b9b641f42 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -1,5 +1,5 @@ import React, { - FC, memo, useCallback, useEffect, useMemo, useRef, useState, + FC, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../lib/teact/teact'; import { getDispatch } from '../../lib/teact/teactn'; @@ -18,7 +18,7 @@ import { isMessageLocal, isOwnMessage, } from '../../modules/helpers'; -import { renderWaveformToDataUri } from './helpers/waveform'; +import { renderWaveform } from './helpers/waveform'; import buildClassName from '../../util/buildClassName'; import renderText from './helpers/renderText'; import { getFileSizeString } from './helpers/documentInfo'; @@ -85,7 +85,7 @@ const Audio: FC = ({ const isSeeking = useRef(false); const playStateBeforeSeeking = useRef(false); // eslint-disable-next-line no-null/no-null - const seekerRef = useRef(null); + const seekerRef = useRef(null); const lang = useLang(); const { isRtl } = lang; const dispatch = getDispatch(); @@ -136,6 +136,9 @@ const Audio: FC = ({ isMessageLocal(message), ); + const isOwn = isOwnMessage(message); + const waveformCanvasRef = useWaveformCanvas(theme, voice, (isMediaUnread && !isOwn) ? 1 : playProgress, isOwn); + const withSeekline = isPlaying || (playProgress > 0 && playProgress < 1); useEffect(() => { @@ -224,7 +227,7 @@ const Audio: FC = ({ }); }, [withSeekline, handleStartSeek, handleSeek, handleStopSeek]); - function getFirstLine() { + function renderFirstLine() { if (isVoice) { return senderTitle || 'Voice'; } @@ -234,7 +237,7 @@ const Audio: FC = ({ return title || fileName; } - function getSecondLine() { + function renderSecondLine() { if (isVoice) { return (
@@ -256,18 +259,6 @@ const Audio: FC = ({ ); } - 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( 'Audio', className, @@ -292,7 +283,7 @@ const Audio: FC = ({ <>
-

{renderText(getFirstLine())}

+

{renderText(renderFirstLine())}

{date && ( @@ -314,7 +305,7 @@ const Audio: FC = ({ {renderSeekline(playProgress, bufferedProgress, seekerRef)}
)} - {!withSeekline && getSecondLine()} + {!withSeekline && renderSecondLine()}
); @@ -369,7 +360,9 @@ const Audio: FC = ({ (isDownloading || isUploading), date, transferProgress, onDateClick ? handleDateClick : undefined, )} {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) + )}
); }; @@ -428,10 +421,22 @@ function renderAudio( ); } -function renderVoice(voice: ApiVoice, renderedWaveform: any, playProgress: number, isMediaUnread?: boolean) { +function renderVoice( + voice: ApiVoice, + seekerRef: React.Ref, + waveformCanvasRef: React.Ref, + playProgress: number, + isMediaUnread?: boolean, +) { return (
- {renderedWaveform} +
+ +

{playProgress === 0 ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}

@@ -439,45 +444,51 @@ function renderVoice(voice: ApiVoice, renderedWaveform: any, playProgress: numbe ); } -function renderWaveform( - voice: ApiVoice, +function useWaveformCanvas( + theme: ISettings['theme'], + voice?: ApiVoice, playProgress = 0, isOwn = false, - theme: ISettings['theme'], - seekerRef: React.Ref, ) { - const { waveform, duration } = voice; + // eslint-disable-next-line no-null/no-null + const canvasRef = useRef(null); - if (!waveform) { - return undefined; - } + const { data: spikes, peak } = useMemo(() => { + if (!voice) { + return undefined; + } - const fillColor = theme === 'dark' ? '#494A78' : '#ADD3F7'; - const fillOwnColor = theme === 'dark' ? '#B7ABED' : '#AEDFA4'; - const progressFillColor = theme === 'dark' ? '#8774E1' : '#3390EC'; - 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, - }); + const { waveform, duration } = voice; + if (!waveform) { + return undefined; + } - return ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions - } - /> - ); + 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)); + + return interpolateArray(decodedWaveform, spikesCount); + }, [voice]) || {}; + + useLayoutEffect(() => { + const canvas = canvasRef.current; + 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( diff --git a/src/components/common/helpers/waveform.ts b/src/components/common/helpers/waveform.ts index 1738ffe2c..06b768825 100644 --- a/src/components/common/helpers/waveform.ts +++ b/src/components/common/helpers/waveform.ts @@ -9,7 +9,8 @@ const SPIKE_STEP = 4; const SPIKE_RADIUS = 1; const HEIGHT = 23; -export function renderWaveformToDataUri( +export function renderWaveform( + canvas: HTMLCanvasElement, spikes: number[], progress: number, { @@ -19,7 +20,6 @@ export function renderWaveformToDataUri( const width = spikes.length * SPIKE_STEP; const height = HEIGHT; - const canvas = document.createElement('canvas'); canvas.width = width * 2; canvas.height = height * 2; canvas.style.width = `${width}px`; @@ -35,12 +35,6 @@ export function renderWaveformToDataUri( roundedRectangle(ctx, i * SPIKE_STEP, height, SPIKE_WIDTH, spikeHeight, SPIKE_RADIUS); ctx.fill(); }); - - return { - src: canvas.toDataURL(), - width, - height, - }; } function roundedRectangle(