-
-
Notifications
You must be signed in to change notification settings - Fork 91
Feat/android fgs survive task removal #1234
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: main
Are you sure you want to change the base?
Changes from all commits
7a2de61
ac3fc60
fffc428
285e336
7e70c71
1f36f53
57630cc
4818d58
4b82eda
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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,7 @@ import { | |||||||||
| AudioBuffer, | ||||||||||
| AudioBufferSourceNode, | ||||||||||
| AudioManager, | ||||||||||
| concatAudioFiles, | ||||||||||
| AudioRecorder, | ||||||||||
| FileFormat, | ||||||||||
| RecordingNotificationManager, | ||||||||||
| } from 'react-native-audio-api'; | ||||||||||
|
|
@@ -21,7 +21,17 @@ import Status from './Status'; | |||||||||
| import { RecordingState } from './types'; | ||||||||||
|
|
||||||||||
| const Record: FC = () => { | ||||||||||
| const [state, setState] = useState<RecordingState>(RecordingState.Idle); | ||||||||||
| // A recording can outlive this screen (and, with `stopWithTask: false`, the whole | ||||||||||
| // app UI). Mounting directly in the right state lets every child initialize from | ||||||||||
| // the live recorder instead of transitioning out of a transient Idle render. | ||||||||||
|
Comment on lines
+24
to
+26
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.
Suggested change
|
||||||||||
| const [state, setState] = useState<RecordingState>(() => { | ||||||||||
| if (!AudioRecorder.isRecordingOngoing()) { | ||||||||||
| return RecordingState.Idle; | ||||||||||
| } | ||||||||||
| return Recorder.isPaused() | ||||||||||
| ? RecordingState.Paused | ||||||||||
| : RecordingState.Recording; | ||||||||||
| }); | ||||||||||
| const [hasPermissions, setHasPermissions] = useState<boolean>(false); | ||||||||||
| const [recordedBuffer, setRecordedBuffer] = useState<AudioBuffer | null>( | ||||||||||
| null | ||||||||||
|
|
@@ -51,9 +61,10 @@ const Record: FC = () => { | |||||||||
| contentText: paused ? 'Paused recording' : 'Recording...', | ||||||||||
| paused, | ||||||||||
| smallIconResourceName: 'logo', | ||||||||||
| pauseIconResourceName: 'pause', | ||||||||||
| resumeIconResourceName: 'resume', | ||||||||||
| color: 0xff6200, | ||||||||||
| showStopAction: true, | ||||||||||
| deepLinkUri: 'audioapi-example://record', | ||||||||||
| usesChronometer: true, | ||||||||||
| }); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
|
|
@@ -118,6 +129,19 @@ const Record: FC = () => { | |||||||||
| setState(RecordingState.Recording); | ||||||||||
| }, []); | ||||||||||
|
|
||||||||||
| const loadRecordedAudio = useCallback( | ||||||||||
| async (paths: string[]) => { | ||||||||||
| setState(RecordingState.Loading); | ||||||||||
|
|
||||||||||
| const audioBuffer = await audioContext.decodeAudioData(paths[0]); | ||||||||||
| setRecordedBuffer(audioBuffer); | ||||||||||
|
|
||||||||||
| setState(RecordingState.ReadyToPlay); | ||||||||||
| currentPositionSV.value = 0; | ||||||||||
| }, | ||||||||||
| [currentPositionSV] | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| const onStopRecording = useCallback(async () => { | ||||||||||
| const info = await Recorder.stop(); | ||||||||||
| RecordingNotificationManager.hide(); | ||||||||||
|
|
@@ -130,15 +154,22 @@ const Record: FC = () => { | |||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const outputPath = info.paths[0].replace(/[^/]+$/, 'recording.m4a'); | ||||||||||
| await loadRecordedAudio(info.paths); | ||||||||||
| }, [loadRecordedAudio]); | ||||||||||
|
|
||||||||||
| const finalPath = await concatAudioFiles(info.paths, outputPath); | ||||||||||
| const audioBuffer = await audioContext.decodeAudioData(finalPath); | ||||||||||
| setRecordedBuffer(audioBuffer); | ||||||||||
| // The stop action already stopped the recorder natively and hid the notification; | ||||||||||
| // here we only pick up the resulting files and sync the UI. | ||||||||||
|
Comment on lines
+160
to
+161
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.
Suggested change
|
||||||||||
| const onStopRecordingFromNotification = useCallback(async () => { | ||||||||||
| const info = AudioRecorder.takeLastRecordingResult(); | ||||||||||
|
|
||||||||||
| setState(RecordingState.ReadyToPlay); | ||||||||||
| currentPositionSV.value = 0; | ||||||||||
| }, []); | ||||||||||
| if (!info || info.paths.length === 0) { | ||||||||||
| setRecordedBuffer(null); | ||||||||||
| setState(RecordingState.Idle); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| await loadRecordedAudio(info.paths); | ||||||||||
| }, [loadRecordedAudio]); | ||||||||||
|
|
||||||||||
| const onPlayRecording = useCallback(() => { | ||||||||||
| if (state !== RecordingState.ReadyToPlay) { | ||||||||||
|
|
@@ -229,11 +260,21 @@ const Record: FC = () => { | |||||||||
|
|
||||||||||
| useEffect(() => { | ||||||||||
| (async () => { | ||||||||||
| const permissionStatus = await AudioManager.checkRecordingPermissions(); | ||||||||||
| const recordingPermissionStatus = | ||||||||||
| await AudioManager.checkRecordingPermissions(); | ||||||||||
|
|
||||||||||
| if (permissionStatus === 'Granted') { | ||||||||||
| if (recordingPermissionStatus === 'Granted') { | ||||||||||
| setHasPermissions(true); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const notificationPermissionStatus = | ||||||||||
| await AudioManager.checkNotificationPermissions(); | ||||||||||
| if (notificationPermissionStatus !== 'Granted') { | ||||||||||
| const result = await AudioManager.requestNotificationPermissions(); | ||||||||||
| if (result !== 'Granted') { | ||||||||||
| console.warn('Notification permissions are not granted'); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| })(); | ||||||||||
| }, []); | ||||||||||
|
|
||||||||||
|
|
@@ -254,22 +295,50 @@ const Record: FC = () => { | |||||||||
| } | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| const stopListener = RecordingNotificationManager.addEventListener( | ||||||||||
| 'recordingNotificationStop', | ||||||||||
| () => { | ||||||||||
| console.log('Notification stop action received'); | ||||||||||
| onStopRecordingFromNotification(); | ||||||||||
| } | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| return () => { | ||||||||||
| pauseListener.remove(); | ||||||||||
| resumeListener.remove(); | ||||||||||
| RecordingNotificationManager.hide(); | ||||||||||
| stopListener.remove(); | ||||||||||
| }; | ||||||||||
| }, [onPauseRecording, onResumeRecording]); | ||||||||||
| }, [onPauseRecording, onResumeRecording, onStopRecordingFromNotification]); | ||||||||||
|
|
||||||||||
| // An ongoing recording is picked up by the state initializer above; here we only | ||||||||||
| // collect the files of a recording that was stopped natively (notification stop | ||||||||||
| // action) while this screen was unmounted. | ||||||||||
|
Comment on lines
+313
to
+315
Collaborator
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.
Suggested change
|
||||||||||
| useEffect(() => { | ||||||||||
| if (AudioRecorder.isRecordingOngoing()) { | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const info = AudioRecorder.takeLastRecordingResult(); | ||||||||||
| if (info && info.paths.length > 0) { | ||||||||||
| loadRecordedAudio(info.paths); | ||||||||||
| } | ||||||||||
| }, [loadRecordedAudio]); | ||||||||||
|
|
||||||||||
| useEffect(() => { | ||||||||||
| Recorder.enableFileOutput({ rotateIntervalBytes: 1_000_000, format: FileFormat.M4A }); | ||||||||||
| // Re-enabling file output during an ongoing recording replaces the file writer, | ||||||||||
| // which starts a new file and resets the duration — skip it when resyncing. | ||||||||||
|
Comment on lines
+328
to
+329
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.
Suggested change
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. might be also worth changing on the native side to handle that and make enable a no-op in that case
Collaborator
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. agree |
||||||||||
| if (!AudioRecorder.isRecordingOngoing()) { | ||||||||||
| Recorder.enableFileOutput({ format: FileFormat.Wav }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return () => { | ||||||||||
| // The recording and its notification intentionally stay alive when leaving this | ||||||||||
| // screen; they can be stopped from the notification or after coming back. | ||||||||||
|
Comment on lines
+335
to
+336
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.
Suggested change
|
||||||||||
| stopPlayback(); | ||||||||||
| Recorder.disableFileOutput(); | ||||||||||
| Recorder.stop(); | ||||||||||
| AudioManager.setAudioSessionActivity(false); | ||||||||||
| RecordingNotificationManager.hide(); | ||||||||||
|
|
||||||||||
| if (!AudioRecorder.isRecordingOngoing()) { | ||||||||||
| AudioManager.setAudioSessionActivity(false); | ||||||||||
| } | ||||||||||
|
Comment on lines
+339
to
+341
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. same here |
||||||||||
| }; | ||||||||||
| }, [stopPlayback]); | ||||||||||
|
|
||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,71 +1,53 @@ | ||||||
| import React, { useEffect } from 'react'; | ||||||
| import { StyleSheet, TextInput } from 'react-native'; | ||||||
| import Animated, { | ||||||
| useAnimatedProps, | ||||||
| useSharedValue, | ||||||
| } from 'react-native-reanimated'; | ||||||
| import React, { useEffect, useState } from 'react'; | ||||||
| import { StyleSheet, Text } from 'react-native'; | ||||||
|
|
||||||
| import { audioRecorder as Recorder } from '../../singletons'; | ||||||
| import { colors } from '../../styles'; | ||||||
| import { RecordingState } from './types'; | ||||||
|
|
||||||
| const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); | ||||||
| const IDLE_DURATION = '00:00:000'; | ||||||
|
|
||||||
| function formatDuration(elapsedSeconds: number) { | ||||||
| const minutes = Math.floor((elapsedSeconds % 3600) / 60) | ||||||
| .toString() | ||||||
| .padStart(2, '0'); | ||||||
| const seconds = Math.floor(elapsedSeconds % 60) | ||||||
| .toString() | ||||||
| .padStart(2, '0'); | ||||||
| const milliseconds = Math.floor((elapsedSeconds % 1) * 1000) | ||||||
| .toString() | ||||||
| .padStart(3, '0'); | ||||||
|
|
||||||
| return `${minutes}:${seconds}:${milliseconds}`; | ||||||
| } | ||||||
|
|
||||||
| interface RecordingTimeProps { | ||||||
| state: RecordingState; | ||||||
| } | ||||||
|
|
||||||
| const RecordingTime: React.FC<RecordingTimeProps> = ({ state }) => { | ||||||
| const durationStringSV = useSharedValue('00:00:000'); | ||||||
| const isMountedSV = useSharedValue(true); | ||||||
| const [durationString, setDurationString] = useState(IDLE_DURATION); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| isMountedSV.value = true; | ||||||
| if (![RecordingState.Recording, RecordingState.Paused].includes(state)) { | ||||||
| durationStringSV.value = '00:00:00'; | ||||||
| setDurationString(IDLE_DURATION); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| const interval = setInterval(() => { | ||||||
| if (!isMountedSV.value) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| const elapsedSeconds = Recorder.getCurrentDuration(); | ||||||
| const refreshDuration = () => | ||||||
| setDurationString(formatDuration(Recorder.getCurrentDuration())); | ||||||
|
|
||||||
| const minutes = Math.floor((elapsedSeconds % 3600) / 60) | ||||||
| .toString() | ||||||
| .padStart(2, '0'); | ||||||
| const seconds = Math.floor(elapsedSeconds % 60) | ||||||
| .toString() | ||||||
| .padStart(2, '0'); | ||||||
| const milliseconds = Math.floor((elapsedSeconds % 1) * 1000) | ||||||
| .toString() | ||||||
| .padStart(3, '0'); | ||||||
|
|
||||||
| durationStringSV.value = `${minutes}:${seconds}:${milliseconds}`; | ||||||
| }, 100); | ||||||
| // Also refresh immediately so a paused or resynced screen shows the real | ||||||
| // duration before the first interval tick. | ||||||
|
Comment on lines
+40
to
+41
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.
Suggested change
|
||||||
| refreshDuration(); | ||||||
| const interval = setInterval(refreshDuration, 100); | ||||||
|
|
||||||
| return () => { | ||||||
| isMountedSV.value = false; | ||||||
| clearInterval(interval); | ||||||
| }; | ||||||
| }, [state, durationStringSV, isMountedSV]); | ||||||
|
|
||||||
| const animatedText = useAnimatedProps(() => { | ||||||
| return { | ||||||
| text: durationStringSV.value, | ||||||
| defaultValue: '00:00:000', | ||||||
| }; | ||||||
| }); | ||||||
| }, [state]); | ||||||
|
|
||||||
| return ( | ||||||
| <AnimatedTextInput | ||||||
| editable={false} | ||||||
| animatedProps={animatedText} | ||||||
| style={styles.text} | ||||||
| /> | ||||||
| ); | ||||||
| return <Text style={styles.text}>{durationString}</Text>; | ||||||
|
Collaborator
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 we moved to state driven timer? |
||||||
| }; | ||||||
|
|
||||||
| export default RecordingTime; | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.