diff --git a/.claude/skills/turbo-modules/SKILL.md b/.claude/skills/turbo-modules/SKILL.md index 3c6372582..68257e1e2 100644 --- a/.claude/skills/turbo-modules/SKILL.md +++ b/.claude/skills/turbo-modules/SKILL.md @@ -218,7 +218,7 @@ void AudioAPIModule::registerNatives() { `registerNatives()` is called from `android/src/main/cpp/audioapi/android/OnLoad.cpp` at `.so` load time. -**`invokeHandlerWithEventNameAndEventBody`**: called from Kotlin (MediaSessionManager callbacks) to fire events into JS. Takes a Java `Map` and converts to `std::unordered_map`. +**`invokeHandlerWithEventNameAndEventBody`**: called from Kotlin (MediaSessionManager callbacks) to fire events into JS. Takes a Java `Map` and converts it to a typed `AudioEventPayload` in `buildPayloadFromJniMap` (`android/src/main/cpp/audioapi/android/JniEventPayloadParser.cpp`), which switches on the event. An event without a case there falls to `EmptyPayload{}`: the event still reaches JS, but with an empty body. When Kotlin starts sending a new event, add its case, building the same payload type the iOS side dispatches (e.g. `ROUTE_CHANGE` → `StringPayload{.name = "reason", ...}`). --- diff --git a/apps/common-app/src/demos/Record/Record.tsx b/apps/common-app/src/demos/Record/Record.tsx index 37ddb0e2b..0b5e97a65 100644 --- a/apps/common-app/src/demos/Record/Record.tsx +++ b/apps/common-app/src/demos/Record/Record.tsx @@ -7,10 +7,11 @@ import { concatAudioFiles, FileFormat, RecordingNotificationManager, + useAudioInput, } from 'react-native-audio-api'; import { Alert, StyleSheet, View } from 'react-native'; -import { Container } from '../../components'; +import { Container, Select } from '../../components'; import { Easing, useSharedValue, withTiming } from 'react-native-reanimated'; import { audioRecorder as Recorder, audioContext } from '../../singletons'; @@ -37,6 +38,28 @@ const Record: FC = () => { ); const currentPositionSV = useSharedValue(0); const playbackSourceRef = useRef(null); + const { availableInputs, currentInput, onSelectInput } = useAudioInput(); + + const inputLabels = availableInputs.map( + (device) => `${device.name} · ${device.category} #${device.id}` + ); + const currentInputLabel = currentInput + ? inputLabels[availableInputs.indexOf(currentInput)] + : 'System default'; + + const onSelectInputLabel = async (label: string) => { + const input = availableInputs[inputLabels.indexOf(label)]; + + if (!input) { + return; + } + + try { + await onSelectInput(input); + } catch (error) { + Alert.alert('Input Device Error', `${error}`); + } + }; const stopPlayback = useCallback(() => { const source = playbackSourceRef.current; @@ -284,6 +307,16 @@ const Record: FC = () => { })(); }, []); + useEffect(() => { + Recorder.onError((error) => { + Alert.alert('Recorder Error', error.message); + }); + + return () => { + Recorder.clearOnError(); + }; + }, []); + useEffect(() => { const pauseListener = RecordingNotificationManager.addEventListener( 'recordingNotificationPause', @@ -344,6 +377,13 @@ const Record: FC = () => { return ( + +