-
Notifications
You must be signed in to change notification settings - Fork 1.5k
chore: migrate from expo-av to expo-audio and expo-video #6719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
8f79396
0deffc8
ac0b630
2667db7
2561d80
f58a536
23ec327
abab68f
1514c7e
5f28192
c0a4d25
f27e9f5
14ecf41
58b5141
a624049
00af6b8
d181c65
d3f516b
b2c221d
5e48723
261b939
b64c1eb
e1eaf31
1998f31
7e7f3d0
64a72f6
998504c
691fb63
caedf9e
cbfe560
8f89ea2
65be6cf
08056fb
b4a596f
76cc834
f03df70
e13c5a4
4fe2c97
64f4dd9
1dba726
caf9d5a
b0d5d9f
e3ccc64
2f535e3
3776737
7f04181
40d9fb9
f868d6d
504d378
54698cb
9ed2a82
baca611
fd97518
deb038d
6afa789
3937eec
acd8675
854b508
9feee82
b5aaff9
6413650
d0f3d39
a8cdebd
ebe4419
b054ed4
bcbc7fe
8a0d12c
02cd74f
e50f3ad
af3098d
b7c2b2f
22ff512
cc271ef
eb37e68
b5a87b7
a0a6854
ebd8f88
f4defa6
d66c812
7662c7f
88e8b42
c19cadf
7946eb4
d6a3237
6fc21de
7d1f242
301b0c3
8daab95
d23a7e0
7c02045
38cd2c9
930c88a
d0d218b
3bab462
cd35041
35b55df
50e1ffc
ec14b28
28d9062
fb40b8b
ae5df9f
fdaf544
05c7b5a
5064359
650104e
7144138
8db5f90
d3f23e7
c03938c
4e7cc2b
0e45349
dbffe97
d33a796
719c192
1e6da08
e8abcf1
8b410dc
b81a3ae
4a7a9fb
709d7d8
9e60c47
449360a
9b1713c
3dc77ca
9c25b88
9ff7576
28d8d4e
1ddf1c6
54c2e9d
a367e02
2b3e51a
5756896
45d1292
ac20a76
cdda960
cf08b03
cff02b6
c48a040
175bd53
1d7d89d
bf5c915
5042da6
912278c
2f1ba4a
59cae3a
1529a23
4ed67ee
0232059
01552c7
a603042
e9590e3
063ce88
f80ab8e
53d9751
cdf85b0
4ff6637
c2a6ca4
31d5e63
be5e8e9
cfc5f2e
2116a2d
c8a5cce
291143b
bdf46e7
21f08c8
fd24f10
8048a08
fbe735c
05cacf5
be81a47
f3c0d50
6929797
d327896
09d63d8
951558d
b4cd9b5
ee7133d
7f5e04d
88be3cd
6bb597e
03e0ae0
a130cd1
a541660
6e35a0b
49eb0d8
6d0ef3a
4688922
857ea2c
8f9b788
5646bdf
9ca79ca
d169c97
0125108
75e2166
ee646a4
062be8f
10fd51f
41aaa31
c9ac652
f281134
4db9e2f
59278db
b01fe2c
e1c28fd
78f1f18
81881b9
380197d
dd072db
097117b
0725500
60f1eba
f9cc233
e38798f
13ca710
a819401
27182e1
8682447
087c7e8
3b9a6a9
50e459e
96d5b49
6ef935f
97f4d75
ed07693
d74c32a
b7f988d
f761897
1f3a18d
a354032
0bea258
424f719
56be8c6
01d3838
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,7 +20,7 @@ interface ISeek { | |||||||||||||||||||||||||||||||||||||||||
| duration: SharedValue<number>; | ||||||||||||||||||||||||||||||||||||||||||
| currentTime: SharedValue<number>; | ||||||||||||||||||||||||||||||||||||||||||
| loaded: boolean; | ||||||||||||||||||||||||||||||||||||||||||
| onChangeTime: (time: number) => Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||
| onChangeTime: (time: number) => void; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| function clamp(value: number, min: number, max: number) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -49,6 +49,8 @@ const Seek = ({ currentTime, duration, loaded = false, onChangeTime }: ISeek) => | |||||||||||||||||||||||||||||||||||||||||
| const scale = useSharedValue(1); | ||||||||||||||||||||||||||||||||||||||||||
| const isPanning = useSharedValue(false); | ||||||||||||||||||||||||||||||||||||||||||
| const contextX = useSharedValue(0); | ||||||||||||||||||||||||||||||||||||||||||
| const savedTranslateX = useSharedValue(0); | ||||||||||||||||||||||||||||||||||||||||||
| const savedCurrentTime = useSharedValue(0); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const styleLine = useAnimatedStyle(() => ({ | ||||||||||||||||||||||||||||||||||||||||||
| width: translateX.value | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -69,23 +71,24 @@ const Seek = ({ currentTime, duration, loaded = false, onChangeTime }: ISeek) => | |||||||||||||||||||||||||||||||||||||||||
| .onStart(() => { | ||||||||||||||||||||||||||||||||||||||||||
| isPanning.value = true; | ||||||||||||||||||||||||||||||||||||||||||
| contextX.value = translateX.value; | ||||||||||||||||||||||||||||||||||||||||||
| savedTranslateX.value = translateX.value; | ||||||||||||||||||||||||||||||||||||||||||
| savedCurrentTime.value = currentTime.value; | ||||||||||||||||||||||||||||||||||||||||||
| scale.value = withTiming(1.3, { duration: 150 }); | ||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||
| .onUpdate(event => { | ||||||||||||||||||||||||||||||||||||||||||
| const newX = contextX.value + event.translationX; | ||||||||||||||||||||||||||||||||||||||||||
| translateX.value = clamp(newX, 0, maxWidth.value); | ||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||
| .onEnd(() => { | ||||||||||||||||||||||||||||||||||||||||||
| scheduleOnRN(onChangeTime, Math.round(currentTime.value * 1000)); | ||||||||||||||||||||||||||||||||||||||||||
| scheduleOnRN(onChangeTime, currentTime.value); | ||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||
| .onFinalize((_, didSucceed) => { | ||||||||||||||||||||||||||||||||||||||||||
| if (isPanning.value && !didSucceed) { | ||||||||||||||||||||||||||||||||||||||||||
| translateX.value = contextX.value; | ||||||||||||||||||||||||||||||||||||||||||
| currentTime.value = (contextX.value * duration.value) / maxWidth.value || 0; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| .onFinalize(didSucceed => { | ||||||||||||||||||||||||||||||||||||||||||
| isPanning.value = false; | ||||||||||||||||||||||||||||||||||||||||||
| scale.value = withTiming(1, { duration: 150 }); | ||||||||||||||||||||||||||||||||||||||||||
| if (!didSucceed) { | ||||||||||||||||||||||||||||||||||||||||||
| translateX.value = savedTranslateX.value; | ||||||||||||||||||||||||||||||||||||||||||
| currentTime.value = savedCurrentTime.value; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+85
to
92
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes—when using RNGH2’s Pan gesture, onFinalize is invoked even if the gesture never satisfies the activation criteria (e.g., activeOffsetX not met), because onFinalize runs when the gesture transitions to END/FAILED/CANCELLED as long as the gesture has begun (BEGAN), regardless of whether it became ACTIVE. In that failure-to-activate case, didSucceed is false (success argument is false when the gesture transitions to FAILED or CANCELLED rather than END). Citations:
Guard the rollback against gestures that never activated.
Proposed fix .onEnd(() => {
scheduleOnRN(onChangeTime, currentTime.value);
})
.onFinalize(didSucceed => {
+ const wasPanning = isPanning.value;
isPanning.value = false;
scale.value = withTiming(1, { duration: 150 });
- if (!didSucceed) {
+ if (!didSucceed && wasPanning) {
translateX.value = savedTranslateX.value;
currentTime.value = savedCurrentTime.value;
}
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| useDerivedValue(() => { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import { InteractionManager, View } from 'react-native'; | ||
| import { type AVPlaybackStatus } from 'expo-av'; | ||
| import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; | ||
| import { useSharedValue } from 'react-native-reanimated'; | ||
| import { useNavigation } from '@react-navigation/native'; | ||
| import type { AudioStatus } from 'expo-audio'; | ||
|
|
||
| import { useTheme } from '../../theme'; | ||
| import styles from './styles'; | ||
|
|
@@ -47,35 +47,34 @@ | |
| const audioUri = useRef<string>(''); | ||
| const navigation = useNavigation(); | ||
|
|
||
| const onPlaybackStatusUpdate = (status: AVPlaybackStatus) => { | ||
| const onPlaybackStatusUpdate = (status: AudioStatus) => { | ||
| if (status) { | ||
| onPlaying(status); | ||
| handlePlaybackStatusUpdate(status); | ||
| onEnd(status); | ||
| } | ||
| }; | ||
|
|
||
| const onPlaying = (data: AVPlaybackStatus) => { | ||
| if (data.isLoaded && data.isPlaying) { | ||
| const onPlaying = (data: AudioStatus) => { | ||
| if (data.isLoaded && data.playing) { | ||
| setPaused(false); | ||
| } else { | ||
| setPaused(true); | ||
| } | ||
| }; | ||
|
|
||
| const handlePlaybackStatusUpdate = (data: AVPlaybackStatus) => { | ||
| if (data.isLoaded && data.durationMillis) { | ||
| const durationSeconds = data.durationMillis / 1000; | ||
| duration.value = durationSeconds > 0 ? durationSeconds : 0; | ||
| const currentSecond = data.positionMillis / 1000; | ||
| if (currentSecond <= durationSeconds) { | ||
| currentTime.value = currentSecond; | ||
| } | ||
| const handlePlaybackStatusUpdate = (data: AudioStatus) => { | ||
| if (!data.isLoaded) return; | ||
| const durationSeconds = data.duration ?? 0; | ||
| duration.value = durationSeconds > 0 ? durationSeconds : 0; | ||
| const currentSecond = data.currentTime ?? 0; | ||
|
Comment on lines
+68
to
+70
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why |
||
| if (currentSecond <= durationSeconds) { | ||
| currentTime.value = currentSecond; | ||
| } | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const onEnd = (data: AVPlaybackStatus) => { | ||
| if (data.isLoaded && data.didJustFinish) { | ||
| const onEnd = (data: AudioStatus) => { | ||
| if (data.isLoaded && data.playbackState === 'ended') { | ||
| try { | ||
| setPaused(true); | ||
| currentTime.value = 0; | ||
|
|
@@ -85,8 +84,8 @@ | |
| } | ||
| }; | ||
|
|
||
| const setPosition = async (time: number) => { | ||
| await AudioManager.setPositionAsync(audioUri.current, time); | ||
| const setPosition = (time: number) => { | ||
| AudioManager.setPositionAsync(audioUri.current, time); | ||
| }; | ||
|
|
||
| const togglePlayPause = async () => { | ||
|
|
@@ -128,7 +127,7 @@ | |
| }); | ||
| return () => task.cancel(); | ||
| } | ||
| }, [fileUri, isDownloaded]); | ||
|
Check warning on line 130 in app/containers/AudioPlayer/index.tsx
|
||
|
|
||
| useEffect(() => { | ||
| if (paused) { | ||
|
|
@@ -151,7 +150,7 @@ | |
| unsubscribeFocus(); | ||
| unsubscribeBlur(); | ||
| }; | ||
| }, [navigation]); | ||
|
Check warning on line 153 in app/containers/AudioPlayer/index.tsx
|
||
|
|
||
| useEffect(() => { | ||
| const audioFocusedEventHandler = (audioFocused: string) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -1,6 +1,12 @@ | ||||
| import { View, Text } from 'react-native'; | ||||
| import { useEffect, useRef, useState, type ReactElement } from 'react'; | ||||
| import { Audio } from 'expo-av'; | ||||
| import { | ||||
| RecordingPresets, | ||||
| requestRecordingPermissionsAsync, | ||||
| setAudioModeAsync, | ||||
| useAudioRecorder, | ||||
| useAudioRecorderState | ||||
| } from 'expo-audio'; | ||||
| import { getInfoAsync } from 'expo-file-system/legacy'; | ||||
| import { useKeepAwake } from 'expo-keep-awake'; | ||||
| import { shallowEqual } from 'react-redux'; | ||||
|
|
@@ -12,7 +18,7 @@ | |||
| import { ReviewButton } from './ReviewButton'; | ||||
| import { useMessageComposerApi } from '../../context'; | ||||
| import { sendFileMessage } from '../../../../lib/methods/sendFileMessage'; | ||||
| import { RECORDING_EXTENSION, RECORDING_MODE, RECORDING_SETTINGS } from '../../../../lib/constants/audio'; | ||||
| import { RECORDING_EXTENSION } from '../../../../lib/constants/audio'; | ||||
| import { useAppSelector } from '../../../../lib/hooks/useAppSelector'; | ||||
| import log from '../../../../lib/methods/helpers/log'; | ||||
| import { type IUpload } from '../../../../definitions'; | ||||
|
|
@@ -25,9 +31,11 @@ | |||
|
|
||||
| export const RecordAudio = (): ReactElement | null => { | ||||
| const [styles, colors] = useStyle(); | ||||
| const recordingRef = useRef<Audio.Recording | null>(null); | ||||
| const audioRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); | ||||
| const recorderState = useAudioRecorderState(audioRecorder); | ||||
|
|
||||
| const durationRef = useRef<IDurationRef>({} as IDurationRef); | ||||
| const numberOfTriesRef = useRef(0); | ||||
|
Check failure on line 38 in app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx
|
||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔴 Critical | ⚡ Quick win Remove the unused retry ref to unblock ESLint.
Proposed fix- const numberOfTriesRef = useRef(0);📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 38-38: 'numberOfTriesRef' is assigned a value but never used. ( 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||
| const [status, setStatus] = useState<'recording' | 'reviewing'>('recording'); | ||||
| const { setRecordingAudio } = useMessageComposerApi(); | ||||
| const { rid, tmid } = useRoomContext(); | ||||
|
|
@@ -36,45 +44,44 @@ | |||
| const permissionToUpload = useCanUploadFile(rid); | ||||
| useKeepAwake(); | ||||
|
|
||||
| async function doRecording() { | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason to rename and move away from the effect? |
||||
| const permissions = await requestRecordingPermissionsAsync(); | ||||
| if (!permissions.granted) { | ||||
| setRecordingAudio(false); | ||||
| return; | ||||
| } | ||||
|
|
||||
| await setAudioModeAsync({ | ||||
| playsInSilentMode: true, | ||||
| allowsRecording: true | ||||
| }); | ||||
|
|
||||
| await audioRecorder.prepareToRecordAsync(); | ||||
| await audioRecorder.record(); | ||||
| } | ||||
|
|
||||
| useEffect(() => { | ||||
| const record = async () => { | ||||
| try { | ||||
| await Audio.setAudioModeAsync(RECORDING_MODE); | ||||
| recordingRef.current = new Audio.Recording(); | ||||
| await recordingRef.current.prepareToRecordAsync(RECORDING_SETTINGS); | ||||
| recordingRef.current.setOnRecordingStatusUpdate(durationRef.current.onRecordingStatusUpdate); | ||||
| await recordingRef.current.startAsync(); | ||||
| } catch (error: any) { | ||||
| // error only occurs on iOS devices | ||||
| if (error?.code === 'E_AUDIO_RECORDERNOTCREATED') { | ||||
| if (numberOfTriesRef.current <= 5) { | ||||
| recordingRef.current = null; | ||||
| numberOfTriesRef.current += 1; | ||||
| setTimeout(() => { | ||||
| record(); | ||||
| }, 100); | ||||
| } else { | ||||
| console.error(error); | ||||
| } | ||||
| } else { | ||||
| console.error(error); | ||||
| } | ||||
| } | ||||
| }; | ||||
| record(); | ||||
| doRecording().catch(error => { | ||||
| log(error); | ||||
| setRecordingAudio(false); | ||||
| }); | ||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||
|
|
||||
| return () => { | ||||
| try { | ||||
| recordingRef.current?.stopAndUnloadAsync(); | ||||
| } catch { | ||||
| audioRecorder.stop().catch(() => { | ||||
| // Do nothing | ||||
| } | ||||
| }); | ||||
| }; | ||||
| }, []); | ||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||
|
|
||||
| useEffect(() => { | ||||
| if (!durationRef.current) return; | ||||
|
|
||||
| durationRef.current.onRecordingStatusUpdate?.(recorderState); | ||||
| }, [recorderState]); | ||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||
|
|
||||
| const cancelRecording = async () => { | ||||
| try { | ||||
| await recordingRef.current?.stopAndUnloadAsync(); | ||||
| await audioRecorder.stop(); | ||||
| } catch { | ||||
| // Do nothing | ||||
| } finally { | ||||
|
|
@@ -84,7 +91,7 @@ | |||
|
|
||||
| const goReview = async () => { | ||||
| try { | ||||
| await recordingRef.current?.stopAndUnloadAsync(); | ||||
| await audioRecorder.stop(); | ||||
| setStatus('reviewing'); | ||||
| } catch { | ||||
| // Do nothing | ||||
|
|
@@ -95,14 +102,13 @@ | |||
| try { | ||||
| if (!rid) return; | ||||
| setRecordingAudio(false); | ||||
| const fileURI = recordingRef.current?.getURI(); | ||||
| const fileData = await getInfoAsync(fileURI as string); | ||||
| const fileData = await getInfoAsync(audioRecorder.uri as string); | ||||
| const fileInfo = { | ||||
| name: `${Date.now()}${RECORDING_EXTENSION}`, | ||||
| mime: 'audio/aac', | ||||
| type: 'audio/aac', | ||||
| store: 'Uploads', | ||||
| path: fileURI, | ||||
| path: audioRecorder.uri, | ||||
| size: fileData.exists ? fileData.size : null | ||||
| } as IUpload; | ||||
|
|
||||
|
|
@@ -124,7 +130,7 @@ | |||
| return ( | ||||
| <View style={styles.review}> | ||||
| <View style={styles.audioPlayer}> | ||||
| <AudioPlayer fileUri={recordingRef.current?.getURI() ?? ''} rid={rid} downloadState='downloaded' /> | ||||
| <AudioPlayer fileUri={recorderState.url ?? ''} rid={rid} downloadState='downloaded' /> | ||||
| </View> | ||||
| <View style={styles.buttons}> | ||||
| <CancelButton onPress={cancelRecording} /> | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,27 @@ | ||
| import { Audio } from 'expo-av'; | ||
| import { useEffect, useRef, memo } from 'react'; | ||
| import { useAudioPlayer } from 'expo-audio'; | ||
| import { useEffect, memo } from 'react'; | ||
|
|
||
| export enum ERingerSounds { | ||
| DIALTONE = 'dialtone', | ||
| RINGTONE = 'ringtone' | ||
| } | ||
|
|
||
| const RINGER_SOUND_FILES = { | ||
| [ERingerSounds.DIALTONE]: require('./dialtone.mp3'), | ||
| [ERingerSounds.RINGTONE]: require('./ringtone.mp3') | ||
| } as const; | ||
|
|
||
| const Ringer = memo(({ ringer }: { ringer: ERingerSounds }) => { | ||
| const sound = useRef(new Audio.Sound()); | ||
| const player = useAudioPlayer(RINGER_SOUND_FILES[ringer]); | ||
|
|
||
| useEffect(() => { | ||
| const loadAndPlay = async () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason to remove the conditional loading? |
||
| try { | ||
| const soundFile = ringer === ERingerSounds.DIALTONE ? require(`./dialtone.mp3`) : require(`./ringtone.mp3`); | ||
| await sound.current.loadAsync(soundFile); | ||
| await sound.current.playAsync(); | ||
| await sound.current.setIsLoopingAsync(true); | ||
| } catch (error) { | ||
| console.error('Error loading sound:', error); | ||
| } | ||
| }; | ||
|
|
||
| loadAndPlay(); | ||
|
|
||
| return () => { | ||
| sound.current?.unloadAsync(); | ||
| }; | ||
| }, []); | ||
| try { | ||
| player.loop = true; | ||
| player.play(); | ||
| } catch (error) { | ||
| console.error('Error loading sound:', error); | ||
| } | ||
| }, [player]); | ||
|
|
||
| return null; | ||
| }); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.