Skip to content

Commit 322ce32

Browse files
committed
perf: improve wavebar and audio playing performance by a huge margin
1 parent 89d2853 commit 322ce32

File tree

4 files changed

+165
-67
lines changed

4 files changed

+165
-67
lines changed

package/native-package/src/optionalDependencies/Audio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class _Audio {
136136
};
137137
startPlayer = async (uri, _, onPlaybackStatusUpdate) => {
138138
try {
139-
const playback = await audioRecorderPlayer.startPlayer(uri);
139+
await audioRecorderPlayer.startPlayer(uri);
140140
audioRecorderPlayer.addPlayBackListener((status) => {
141141
onPlaybackStatusUpdate(status);
142142
});

package/src/components/Attachment/Audio/AudioAttachment.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,6 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
161161
audioPlayer.pause();
162162
};
163163

164-
const dragProgress = (currentProgress: number) => {
165-
audioPlayer.progress = currentProgress;
166-
};
167-
168164
const dragEnd = async (currentProgress: number) => {
169165
const positionInSeconds = (currentProgress * duration) / ONE_SECOND_IN_MILLISECONDS;
170166
await audioPlayer.seek(positionInSeconds);
@@ -262,7 +258,6 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
262258
<WaveProgressBar
263259
isPlaying={isPlaying}
264260
onEndDrag={dragEnd}
265-
onProgressDrag={dragProgress}
266261
onStartDrag={dragStart}
267262
progress={progress}
268263
waveformData={item.waveform_data}

package/src/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { useStateStore } from '../../../../hooks/useStateStore';
1111

1212
import { Pause } from '../../../../icons/Pause';
1313
import { Play } from '../../../../icons/Play';
14-
import { NativeHandlers } from '../../../../native';
1514
import { AudioPlayerState } from '../../../../state-store/audio-player';
1615
import { AudioRecorderManagerState } from '../../../../state-store/audio-recorder-manager';
1716
import { primitives } from '../../../../theme';

package/src/components/ProgressControl/WaveProgressBar.tsx

Lines changed: 164 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import React, { useCallback, useMemo, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
import { StyleSheet, View } from 'react-native';
3+
import type { StyleProp, ViewStyle } from 'react-native';
34
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4-
import Animated, {
5-
runOnJS,
6-
useAnimatedReaction,
7-
useAnimatedStyle,
8-
useSharedValue,
9-
} from 'react-native-reanimated';
5+
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
106

117
import { ProgressControlThumb } from './ProgressThumb';
128

@@ -50,6 +46,37 @@ const WAVEFORM_GAP = 2;
5046
const WAVE_MAX_HEIGHT = 20;
5147
const WAVE_MIN_HEIGHT = 2;
5248

49+
const clampProgress = (progress: number) => {
50+
'worklet';
51+
return Math.max(0, Math.min(progress, 1));
52+
};
53+
54+
type WaveformBarsProps = {
55+
color: string;
56+
heights: number[];
57+
waveformStyle?: StyleProp<ViewStyle>;
58+
};
59+
60+
const WaveformBars = React.memo(({ color, heights, waveformStyle }: WaveformBarsProps) => (
61+
<View style={styles.waveformLayer}>
62+
{heights.map((height, index) => (
63+
<View
64+
key={index}
65+
style={[
66+
styles.waveform,
67+
{
68+
backgroundColor: color,
69+
height,
70+
},
71+
waveformStyle,
72+
]}
73+
/>
74+
))}
75+
</View>
76+
));
77+
78+
WaveformBars.displayName = 'WaveformBars';
79+
5380
export const WaveProgressBar = React.memo(
5481
(props: WaveProgressBarProps) => {
5582
const [width, setWidth] = useState<number>(0);
@@ -62,30 +89,15 @@ export const WaveProgressBar = React.memo(
6289
progress,
6390
waveformData,
6491
} = props;
92+
const [showInteractiveLayer, setShowInteractiveLayer] = useState(
93+
() => progress > 0 || isPlaying,
94+
);
6595
const eachWaveformWidth = WAVEFORM_WIDTH + WAVEFORM_GAP;
6696
const fullWidth = (amplitudesCount - 1) * eachWaveformWidth;
67-
const state = useSharedValue(progress);
68-
const [currentWaveformProgress, setCurrentWaveformProgress] = useState<number>(0);
69-
70-
const waveFormNumberFromProgress = useCallback(
71-
(progress: number) => {
72-
'worklet';
73-
const progressInPrecision = Number(progress.toFixed(2));
74-
const progressInWaveformWidth = Number((progressInPrecision * fullWidth).toFixed(0));
75-
const progressInWaveformNumber = Math.floor(progressInWaveformWidth / 4);
76-
runOnJS(setCurrentWaveformProgress)(progressInWaveformNumber);
77-
},
78-
[fullWidth],
79-
);
80-
81-
useAnimatedReaction(
82-
() => progress,
83-
(newProgress) => {
84-
state.value = newProgress;
85-
waveFormNumberFromProgress(newProgress);
86-
},
87-
[progress],
88-
);
97+
const maxProgressWidth = fullWidth + WAVEFORM_WIDTH;
98+
const dragStartProgress = useSharedValue(0);
99+
const isDragging = useSharedValue(false);
100+
const visualProgress = useSharedValue(progress);
89101

90102
const {
91103
theme: {
@@ -94,42 +106,113 @@ export const WaveProgressBar = React.memo(
94106
},
95107
} = useTheme();
96108

109+
useEffect(() => {
110+
if (!isDragging.value) {
111+
visualProgress.value = progress;
112+
}
113+
}, [isDragging, progress, visualProgress]);
114+
115+
useEffect(() => {
116+
setShowInteractiveLayer(progress > 0 || isPlaying);
117+
}, [isPlaying, progress]);
118+
119+
const handleStartDrag = useCallback(
120+
(nextProgress: number) => {
121+
setShowInteractiveLayer(true);
122+
onStartDrag?.(nextProgress);
123+
},
124+
[onStartDrag],
125+
);
126+
127+
const handleProgressDrag = useCallback(
128+
(nextProgress: number) => {
129+
onProgressDrag?.(nextProgress);
130+
},
131+
[onProgressDrag],
132+
);
133+
134+
const handleEndDrag = useCallback(
135+
(nextProgress: number) => {
136+
onEndDrag?.(nextProgress);
137+
},
138+
[onEndDrag],
139+
);
140+
97141
const pan = useMemo(
98142
() =>
99143
Gesture.Pan()
100144
.maxPointers(1)
101145
.onStart(() => {
146+
const nextProgress = clampProgress(visualProgress.value);
147+
dragStartProgress.value = nextProgress;
148+
isDragging.value = true;
102149
if (onStartDrag) {
103-
runOnJS(onStartDrag)(state.value);
150+
runOnJS(handleStartDrag)(nextProgress);
104151
}
105152
})
106153
.onUpdate((event) => {
107-
const newProgress = Math.max(0, Math.min((state.value + event.x) / fullWidth, 1));
108-
state.value = newProgress;
109-
waveFormNumberFromProgress(newProgress);
154+
if (fullWidth <= 0) {
155+
return;
156+
}
157+
const nextProgress = clampProgress(
158+
dragStartProgress.value + event.translationX / fullWidth,
159+
);
160+
visualProgress.value = nextProgress;
161+
if (onProgressDrag) {
162+
runOnJS(handleProgressDrag)(nextProgress);
163+
}
110164
})
111165
.onEnd(() => {
166+
isDragging.value = false;
112167
if (onEndDrag) {
113-
runOnJS(onEndDrag)(state.value);
168+
runOnJS(handleEndDrag)(visualProgress.value);
114169
}
115170
}),
116-
[fullWidth, onEndDrag, onStartDrag, state, waveFormNumberFromProgress],
171+
[
172+
dragStartProgress,
173+
fullWidth,
174+
handleEndDrag,
175+
handleProgressDrag,
176+
handleStartDrag,
177+
isDragging,
178+
onEndDrag,
179+
onProgressDrag,
180+
onStartDrag,
181+
visualProgress,
182+
],
117183
);
118184

119-
const stringifiedWaveformData = waveformData.toString();
185+
const stringifiedWaveformData = useMemo(() => waveformData.toString(), [waveformData]);
120186

121187
const resampledWaveformData = useMemo(
122188
() => resampleWaveformData(waveformData, amplitudesCount),
123189
// eslint-disable-next-line react-hooks/exhaustive-deps
124190
[amplitudesCount, stringifiedWaveformData],
125191
);
126192

193+
const waveformHeights = useMemo(
194+
() =>
195+
resampledWaveformData.map((waveform) =>
196+
waveform * WAVE_MAX_HEIGHT > WAVE_MIN_HEIGHT
197+
? waveform * WAVE_MAX_HEIGHT
198+
: WAVE_MIN_HEIGHT,
199+
),
200+
[resampledWaveformData],
201+
);
202+
203+
const progressOverlayStyles = useAnimatedStyle(
204+
() => ({
205+
width: clampProgress(visualProgress.value) * maxProgressWidth,
206+
}),
207+
[maxProgressWidth],
208+
);
209+
127210
const thumbStyles = useAnimatedStyle(
128211
() => ({
129212
position: 'absolute',
130-
transform: [{ translateX: currentWaveformProgress * eachWaveformWidth }],
213+
transform: [{ translateX: clampProgress(visualProgress.value) * fullWidth }],
131214
}),
132-
[currentWaveformProgress, fullWidth],
215+
[fullWidth],
133216
);
134217

135218
return (
@@ -140,30 +223,33 @@ export const WaveProgressBar = React.memo(
140223
}}
141224
style={[styles.container, container]}
142225
>
143-
{resampledWaveformData.map((waveform, index) => (
226+
<WaveformBars
227+
color={semantics.chatWaveformBar}
228+
heights={waveformHeights}
229+
waveformStyle={waveformTheme}
230+
/>
231+
{showInteractiveLayer ? (
144232
<Animated.View
145-
key={index}
146-
style={[
147-
styles.waveform,
148-
{
149-
backgroundColor:
150-
index < currentWaveformProgress
151-
? semantics.chatWaveformBarPlaying
152-
: semantics.chatWaveformBar,
153-
height:
154-
waveform * WAVE_MAX_HEIGHT > WAVE_MIN_HEIGHT
155-
? waveform * WAVE_MAX_HEIGHT
156-
: WAVE_MIN_HEIGHT,
157-
},
158-
waveformTheme,
159-
]}
160-
/>
161-
))}
162-
{(onEndDrag || onProgressDrag) && (
163-
<Animated.View style={[thumbStyles, thumb]}>
164-
<ProgressControlThumb isPlaying={isPlaying} />
233+
pointerEvents='none'
234+
style={[styles.progressOverlay, progressOverlayStyles]}
235+
>
236+
<WaveformBars
237+
color={semantics.chatWaveformBarPlaying}
238+
heights={waveformHeights}
239+
waveformStyle={waveformTheme}
240+
/>
165241
</Animated.View>
166-
)}
242+
) : null}
243+
{(onEndDrag || onProgressDrag) &&
244+
(showInteractiveLayer ? (
245+
<Animated.View style={[thumbStyles, thumb]}>
246+
<ProgressControlThumb isPlaying={isPlaying} />
247+
</Animated.View>
248+
) : (
249+
<View style={[styles.idleThumb, thumb]}>
250+
<ProgressControlThumb isPlaying={isPlaying} />
251+
</View>
252+
))}
167253
</View>
168254
</GestureDetector>
169255
);
@@ -172,6 +258,9 @@ export const WaveProgressBar = React.memo(
172258
if (prevProps.amplitudesCount !== nextProps.amplitudesCount) {
173259
return false;
174260
}
261+
if (prevProps.isPlaying !== nextProps.isPlaying) {
262+
return false;
263+
}
175264
if (prevProps.progress !== nextProps.progress) {
176265
return false;
177266
} else {
@@ -182,6 +271,21 @@ export const WaveProgressBar = React.memo(
182271

183272
const styles = StyleSheet.create({
184273
container: {
274+
alignItems: 'center',
275+
flexDirection: 'row',
276+
position: 'relative',
277+
},
278+
idleThumb: {
279+
left: 0,
280+
position: 'absolute',
281+
},
282+
progressOverlay: {
283+
left: 0,
284+
overflow: 'hidden',
285+
position: 'absolute',
286+
top: 0,
287+
},
288+
waveformLayer: {
185289
alignItems: 'center',
186290
flexDirection: 'row',
187291
gap: WAVEFORM_GAP,

0 commit comments

Comments
 (0)