Skip to content

Commit d1d77ef

Browse files
committed
feat: introduce centralized audio playback
1 parent 77c7c95 commit d1d77ef

File tree

17 files changed

+514
-266
lines changed

17 files changed

+514
-266
lines changed

package/expo-package/src/optionalDependencies/Sound.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ class ExpoAudioSoundAdapter {
9999

100100
// eslint-disable-next-line require-await
101101
loadAsync = async (initialStatus) => {
102+
// We have to subscribe as early as possible so that we know the initial status(durarion, etc.) of the audio.
103+
this.subscribeStatusEventListener();
102104
this.initialShouldCorrectPitch = initialStatus.shouldCorrectPitch;
103105
this.initialPitchCorrectionQuality = initialStatus.pitchCorrectionQuality;
104106
};
@@ -136,8 +138,7 @@ class ExpoAudioSoundAdapter {
136138
};
137139

138140
// eslint-disable-next-line require-await
139-
setPositionAsync: SoundReturnType['setPositionAsync'] = async (milliseconds) => {
140-
const seconds = milliseconds / 1000;
141+
setPositionAsync: SoundReturnType['setPositionAsync'] = async (seconds) => {
141142
this.player.seekTo(seconds);
142143
};
143144

package/src/components/Attachment/AudioAttachment.tsx

Lines changed: 71 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import React, { RefObject, useEffect, useMemo, useState } from 'react';
1+
import React, { RefObject, useEffect, useMemo } from 'react';
22
import { I18nManager, Pressable, StyleSheet, Text, View } from 'react-native';
33

44
import dayjs from 'dayjs';
55
import duration from 'dayjs/plugin/duration';
66

7-
import { AudioAttachment as StreamAudioAttachment } from 'stream-chat';
7+
import {
8+
isVoiceRecordingAttachment,
9+
AudioAttachment as StreamAudioAttachment,
10+
VoiceRecordingAttachment as StreamVoiceRecordingAttachment,
11+
} from 'stream-chat';
812

913
import { useTheme } from '../../contexts';
10-
import { useAudioPlayer } from '../../hooks/useAudioPlayer';
14+
import { useStateStore } from '../../hooks';
15+
import { useAudioPlayerControl } from '../../hooks/useAudioPlayerControl';
1116
import { Audio, Pause, Play } from '../../icons';
1217
import {
1318
NativeHandlers,
@@ -17,125 +22,100 @@ import {
1722
VideoProgressData,
1823
VideoSeekResponse,
1924
} from '../../native';
20-
import { AudioConfig, FileTypes } from '../../types/types';
25+
import { AudioPlayerState } from '../../state-store/audio-player';
2126
import { getTrimmedAttachmentTitle } from '../../utils/getTrimmedAttachmentTitle';
2227
import { ProgressControl } from '../ProgressControl/ProgressControl';
2328
import { WaveProgressBar } from '../ProgressControl/WaveProgressBar';
2429

2530
dayjs.extend(duration);
2631

27-
export type AudioAttachmentType = AudioConfig &
28-
Pick<StreamAudioAttachment, 'waveform_data' | 'asset_url' | 'title'> & {
29-
id: string;
30-
type: 'audio' | 'voiceRecording';
31-
};
32+
export type AudioAttachmentType = StreamAudioAttachment | StreamVoiceRecordingAttachment;
3233

3334
export type AudioAttachmentProps = {
3435
item: AudioAttachmentType;
35-
onLoad: (index: string, duration: number) => void;
36-
onPlayPause: (index: string, pausedStatus?: boolean) => void;
37-
onProgress: (index: string, progress: number) => void;
3836
titleMaxLength?: number;
3937
hideProgressBar?: boolean;
4038
showSpeedSettings?: boolean;
4139
testID?: string;
4240
};
4341

42+
const audioPlayerSelector = (state: AudioPlayerState) => ({
43+
currentPlaybackRate: state.currentPlaybackRate,
44+
duration: state.duration,
45+
isPlaying: state.isPlaying,
46+
position: state.position,
47+
progress: state.progress,
48+
});
49+
4450
/**
4551
* AudioAttachment
4652
* UI Component to preview the audio files
4753
*/
4854
export const AudioAttachment = (props: AudioAttachmentProps) => {
49-
const [currentSpeed, setCurrentSpeed] = useState<number>(1.0);
50-
const [audioFinished, setAudioFinished] = useState(false);
5155
const soundRef = React.useRef<SoundReturnType | null>(null);
56+
5257
const {
5358
hideProgressBar = false,
5459
item,
55-
onLoad,
56-
onPlayPause,
57-
onProgress,
5860
showSpeedSettings = false,
5961
testID,
6062
titleMaxLength,
6163
} = props;
62-
const { changeAudioSpeed, pauseAudio, playAudio, seekAudio } = useAudioPlayer({ soundRef });
64+
65+
const { audioPlayer, toggleAudio, playAudio, pauseAudio } = useAudioPlayerControl({
66+
duration: item.duration ?? 0,
67+
mimeType: item.mime_type ?? '',
68+
playerRef: soundRef,
69+
uri: item.asset_url ?? '',
70+
});
71+
const { duration, isPlaying, position, progress, currentPlaybackRate } = useStateStore(
72+
audioPlayer.state,
73+
audioPlayerSelector,
74+
);
6375
const isExpoCLI = NativeHandlers.SDK === 'stream-chat-expo';
64-
const isVoiceRecording = item.type === FileTypes.VoiceRecording;
76+
const isVoiceRecording = isVoiceRecordingAttachment(item);
6577

6678
/** This is for Native CLI Apps */
6779
const handleLoad = (payload: VideoPayloadData) => {
6880
// The duration given by the rn-video is not same as the one of the voice recording, so we take the actual duration for voice recording.
6981
if (isVoiceRecording && item.duration) {
70-
onLoad(item.id, item.duration);
82+
audioPlayer.duration = item.duration;
7183
} else {
72-
onLoad(item.id, item.duration || payload.duration);
84+
audioPlayer.duration = item.duration || payload.duration;
7385
}
7486
};
7587

7688
/** This is for Native CLI Apps */
7789
const handleProgress = (data: VideoProgressData) => {
78-
const { currentTime, seekableDuration } = data;
79-
// The duration given by the rn-video is not same as the one of the voice recording, so we take the actual duration for voice recording.
80-
if (isVoiceRecording && item.duration) {
81-
if (currentTime < item.duration && !audioFinished) {
82-
onProgress(item.id, currentTime / item.duration);
83-
} else {
84-
setAudioFinished(true);
85-
}
86-
} else {
87-
if (currentTime < seekableDuration && !audioFinished) {
88-
onProgress(item.id, currentTime / seekableDuration);
89-
} else {
90-
setAudioFinished(true);
91-
}
92-
}
90+
const { currentTime } = data;
91+
audioPlayer.position = currentTime;
9392
};
9493

9594
/** This is for Native CLI Apps */
9695
const onSeek = (seekResponse: VideoSeekResponse) => {
97-
setAudioFinished(false);
98-
onProgress(item.id, seekResponse.currentTime / (item.duration as number));
96+
audioPlayer.position = seekResponse.currentTime;
9997
};
10098

101-
const handlePlayPause = async () => {
102-
if (item.paused) {
103-
if (isExpoCLI) {
104-
await playAudio();
105-
}
106-
onPlayPause(item.id, false);
107-
} else {
108-
if (isExpoCLI) {
109-
await pauseAudio();
110-
}
111-
onPlayPause(item.id, true);
112-
}
99+
const handlePlayPause = () => {
100+
toggleAudio(audioPlayer.id);
113101
};
114102

115103
const handleEnd = async () => {
116-
setAudioFinished(false);
117-
await pauseAudio();
118-
onPlayPause(item.id, true);
119-
await seekAudio(0);
104+
await audioPlayer.stop();
120105
};
121106

122-
const dragStart = async () => {
123-
if (isExpoCLI) {
124-
await pauseAudio();
125-
}
126-
onPlayPause(item.id, true);
107+
const dragStart = () => {
108+
pauseAudio(audioPlayer.id);
127109
};
128110

129111
const dragProgress = (progress: number) => {
130-
onProgress(item.id, progress);
112+
audioPlayer.progress = progress;
131113
};
132114

133115
const dragEnd = async (progress: number) => {
134-
await seekAudio(progress * (item.duration as number));
135-
if (isExpoCLI) {
136-
await playAudio();
137-
}
138-
onPlayPause(item.id, false);
116+
const position = isExpoCLI ? (progress * duration) / 1000 : progress * duration;
117+
await audioPlayer.seek(position);
118+
playAudio(audioPlayer.id);
139119
};
140120

141121
/** For Expo CLI */
@@ -147,26 +127,28 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
147127
}
148128
} else {
149129
const { durationMillis, positionMillis } = playbackStatus;
130+
// Update your UI for the loaded state
150131
// This is done for Expo CLI where we don't get file duration from file picker
151132
if (item.duration === 0) {
152-
onLoad(item.id, durationMillis / 1000);
133+
audioPlayer.duration = durationMillis;
153134
} else {
154135
// The duration given by the expo-av is not same as the one of the voice recording, so we take the actual duration for voice recording.
155136
if (isVoiceRecording && item.duration) {
156-
onLoad(item.id, item.duration);
137+
audioPlayer.duration = item.duration * 1000;
157138
} else {
158-
onLoad(item.id, durationMillis / 1000);
139+
audioPlayer.duration = durationMillis;
159140
}
160141
}
161-
// Update your UI for the loaded state
142+
143+
// Update the position of the audio player when it is playing
162144
if (playbackStatus.isPlaying) {
163145
if (isVoiceRecording && item.duration) {
164146
if (positionMillis <= item.duration * 1000) {
165-
onProgress(item.id, positionMillis / (item.duration * 1000));
147+
audioPlayer.position = positionMillis;
166148
}
167149
} else {
168150
if (positionMillis <= durationMillis) {
169-
onProgress(item.id, positionMillis / durationMillis);
151+
audioPlayer.position = positionMillis;
170152
}
171153
}
172154
} else {
@@ -177,10 +159,8 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
177159
// Update your UI for the buffering state
178160
}
179161

162+
// Update the UI when the audio is finished playing
180163
if (playbackStatus.didJustFinish && !playbackStatus.isLooping) {
181-
onProgress(item.id, 1);
182-
// The player has just finished playing and will stop. Maybe you want to play something else?
183-
// status: opposite of pause,says i am playing
184164
handleEnd();
185165
}
186166
}
@@ -214,41 +194,8 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
214194
// eslint-disable-next-line react-hooks/exhaustive-deps
215195
}, []);
216196

217-
// This is needed for expo applications where the rerender doesn't occur on time thefore you need to update the state of the sound.
218-
useEffect(() => {
219-
const initalPlayPause = async () => {
220-
if (!isExpoCLI) {
221-
return;
222-
}
223-
try {
224-
if (item.paused) {
225-
await pauseAudio();
226-
} else {
227-
await playAudio();
228-
}
229-
} catch (e) {
230-
console.log('An error has occurred while trying to interact with the audio. ', e);
231-
}
232-
};
233-
// For expo CLI
234-
if (!NativeHandlers.Sound?.Player) {
235-
initalPlayPause();
236-
}
237-
}, [item.paused, isExpoCLI, pauseAudio, playAudio]);
238-
239197
const onSpeedChangeHandler = async () => {
240-
if (currentSpeed === 2.0) {
241-
setCurrentSpeed(1.0);
242-
await changeAudioSpeed(1.0);
243-
} else {
244-
if (currentSpeed === 1.0) {
245-
setCurrentSpeed(1.5);
246-
await changeAudioSpeed(1.5);
247-
} else if (currentSpeed === 1.5) {
248-
setCurrentSpeed(2.0);
249-
await changeAudioSpeed(2.0);
250-
}
251-
}
198+
await audioPlayer.changePlaybackRate();
252199
};
253200

254201
const {
@@ -270,19 +217,15 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
270217
},
271218
} = useTheme();
272219

273-
const progressValueInSeconds = useMemo(
274-
() => (item.duration as number) * (item.progress as number),
275-
[item.duration, item.progress],
276-
);
277-
220+
const positionInSeconds = isExpoCLI ? position / 1000 : position;
278221
const progressDuration = useMemo(
279222
() =>
280-
progressValueInSeconds
281-
? progressValueInSeconds / 3600 >= 1
282-
? dayjs.duration(progressValueInSeconds, 'second').format('HH:mm:ss')
283-
: dayjs.duration(progressValueInSeconds, 'second').format('mm:ss')
284-
: dayjs.duration(item.duration ?? 0, 'second').format('mm:ss'),
285-
[progressValueInSeconds, item.duration],
223+
positionInSeconds
224+
? positionInSeconds / 3600 >= 1
225+
? dayjs.duration(positionInSeconds, 'second').format('HH:mm:ss')
226+
: dayjs.duration(positionInSeconds, 'second').format('mm:ss')
227+
: dayjs.duration(isExpoCLI ? duration / 1000 : duration, 'second').format('mm:ss'),
228+
[duration, isExpoCLI, positionInSeconds],
286229
);
287230

288231
return (
@@ -308,7 +251,7 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
308251
playPauseButton,
309252
]}
310253
>
311-
{item.paused ? (
254+
{!isPlaying ? (
312255
<Play fill={static_black} height={32} width={32} />
313256
) : (
314257
<Pause fill={static_black} height={32} width={32} />
@@ -328,7 +271,7 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
328271
filenameText,
329272
]}
330273
>
331-
{item.type === FileTypes.VoiceRecording
274+
{isVoiceRecordingAttachment(item)
332275
? 'Recording'
333276
: getTrimmedAttachmentTitle(item.title, titleMaxLength)}
334277
</Text>
@@ -344,17 +287,17 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
344287
onEndDrag={dragEnd}
345288
onProgressDrag={dragProgress}
346289
onStartDrag={dragStart}
347-
progress={item.progress as number}
290+
progress={progress}
348291
waveformData={item.waveform_data}
349292
/>
350293
) : (
351294
<ProgressControl
352-
duration={item.duration as number}
295+
duration={duration}
353296
filledColor={accent_blue}
354297
onEndDrag={dragEnd}
355298
onProgressDrag={dragProgress}
356299
onStartDrag={dragStart}
357-
progress={item.progress as number}
300+
progress={progress}
358301
testID='progress-control'
359302
/>
360303
)}
@@ -367,8 +310,8 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
367310
onLoad={handleLoad}
368311
onProgress={handleProgress}
369312
onSeek={onSeek}
370-
paused={item.paused}
371-
rate={currentSpeed}
313+
paused={!isPlaying}
314+
rate={currentPlaybackRate}
372315
soundRef={soundRef as RefObject<SoundReturnType>}
373316
testID='sound-player'
374317
uri={item.asset_url}
@@ -377,7 +320,7 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
377320
</View>
378321
{showSpeedSettings ? (
379322
<View style={[styles.rightContainer, rightContainer]}>
380-
{item.paused ? (
323+
{!isPlaying ? (
381324
<Audio fill={'#ffffff'} />
382325
) : (
383326
<Pressable
@@ -390,7 +333,7 @@ export const AudioAttachment = (props: AudioAttachmentProps) => {
390333
>
391334
<Text
392335
style={[styles.speedChangeButtonText, speedChangeButtonText]}
393-
>{`x${currentSpeed.toFixed(1)}`}</Text>
336+
>{`x${currentPlaybackRate.toFixed(1)}`}</Text>
394337
</Pressable>
395338
)}
396339
</View>

0 commit comments

Comments
 (0)