Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions apps/common-app/src/examples/Record/Record.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { FC, useEffect, useState } from 'react';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { Alert, Text, View } from 'react-native';
import {
AudioBuffer,
AudioManager,
useAudioInput,
} from 'react-native-audio-api';

import { Button, Container } from '../../components';
import { Button, Container, Select } from '../../components';
import { colors } from '../../styles';

import { audioContext, audioRecorder } from '../../singletons';
Expand All @@ -18,6 +19,8 @@ enum Status {
}

const Record: FC = () => {
const { availableInputs, currentInput, onSelectInput } = useAudioInput();

const [status, setStatus] = useState<Status>(Status.Idle);
const [capturedBuffers, setCapturedBuffers] = useState<AudioBuffer[]>([]);

Expand All @@ -40,8 +43,8 @@ const Record: FC = () => {

AudioManager.setAudioSessionOptions({
iosCategory: 'playAndRecord',
iosMode: 'default',
iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'],
iosMode: 'voiceChat',
iosOptions: ['allowBluetoothHFP'],
});

const success = await AudioManager.setAudioSessionActivity(true);
Expand Down Expand Up @@ -195,6 +198,17 @@ const Record: FC = () => {
);
};

const onSelect = useCallback(
(uid: string) => {
const input = availableInputs.find((d) => d.uid === uid);

if (input) {
onSelectInput(input);
}
},
[availableInputs, onSelectInput]
);

useEffect(() => {
return () => {
audioRecorder.stop();
Expand All @@ -208,6 +222,13 @@ const Record: FC = () => {
Status: {status}
</Text>
</View>
<View>
<Select
value={currentInput?.uid || ''}
onChange={onSelect}
options={availableInputs.map((d) => d.uid) || []}
/>
</View>
<View style={{ alignItems: 'center', gap: 10 }}>
<Text style={{ color: colors.white, fontSize: 16 }}>Echo</Text>
<Button title="Start Recording" onPress={startEcho} />
Expand Down
2 changes: 1 addition & 1 deletion apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3321,7 +3321,7 @@ SPEC CHECKSUMS:
FBLazyVector: 309703e71d3f2f1ed7dc7889d58309c9d77a95a4
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: f93b5009d8ccd9429fe2a772351980df8a22a413
hermes-engine: 42d6f09ee6ede2feb220e2fb772e8bebb42ca403
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: a41bbdd9af30bf2e5715796b313e44ec43eefff1
RCTRequired: 7be34aabb0b77c3cefe644528df0fa0afad4e4d0
Expand Down
7 changes: 7 additions & 0 deletions packages/audiodocs/docs/react/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"label": "React",
"position": 11,
"link": {
"type": "generated-index"
}
}
69 changes: 69 additions & 0 deletions packages/audiodocs/docs/react/select-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
sidebar_position: 1
---

import { IOS } from '@site/src/components/Badges';

# useAudioInput

<br />

React hook for managing audio input device selection and monitoring available audio input devices. Current input will be available after first activation of the audio session. Not all connected devices might be listed as available inputs, some might be filtered out as incompatible with current session configuration.

The `useAudioInput` hook provides an interface for:
- Retrieving all available audio input devices
- Getting the currently active input device
- Switching between different input devices

<IOS /> **Platform support:** Input device selection is currently only supported on iOS. On Android, `useAudioInput` is implemented as a no-op: the hook will not list or switch input devices, and any selection calls will effectively be ignored.

## Usage

```tsx
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useAudioInput } from 'react-native-audio-api';

function AudioInputSelector() {
const { availableInputs, currentInput, onSelectInput } = useAudioInput();

return (
<View>
<Text>Current Input: {currentInput?.name || 'None'}</Text>

{availableInputs.map((input) => (
<Button
key={input.uid}
title={`${input.name} (${input.category})`}
onPress={() => onSelectInput(input)}
/>
))}
</View>
);
}
```

## Return Value

The hook returns an object with the following properties:

### `availableInputs: AudioDeviceInfo[]`

An array of all available audio input devices. Each device contains:
- `uid: string` - Unique device identifier
- `name: string` - Human-readable device name
- `category: string` - Device category (e.g., "Built-In Microphone", "Bluetooth")

### `currentInput: AudioDeviceInfo | null`

The currently active audio input device, or `null` if no device is selected.

### `onSelectInput: (device: AudioDeviceInfo) => Promise<void>`

Function to programmatically select an audio input device. Takes an `AudioDeviceInfo` object and attempts to set it as the active input device.

## Related

- [AudioManager](/docs/system/audio-manager) - For managing audio sessions and permissions
- [AudioRecorder](/docs/inputs/audio-recorder) - For capturing audio from the selected input device

2 changes: 1 addition & 1 deletion packages/audiodocs/docs/system/audio-manager.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ type IOSOption =
| 'duckOthers'
| 'allowAirPlay'
| 'mixWithOthers'
| 'allowBluetooth'
| 'allowBluetoothHFP'
| 'defaultToSpeaker'
| 'allowBluetoothA2DP'
| 'overrideMutedMicrophoneInterruption'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ class AudioAPIModule(
promise.resolve(MediaSessionManager.getDevicesInfo())
}

override fun setInputDevice(
deviceId: String?,
promise: Promise?,
) {
// TODO: noop for now, but it should be moved to upcoming
// audio engine implementation for android (duplex stream)
Promise.resolve(true)
}

// Notification system methods
@RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS)
override fun showNotification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ class PlaybackNotificationReceiver : BroadcastReceiver() {
ACTION_NOTIFICATION_DISMISSED -> {
audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_DISMISSED.ordinal, mapOf())
}

ACTION_SKIP_FORWARD -> {
val body = HashMap<String, Any>().apply { put("value", 15) }
audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_FORWARD.ordinal, body)
}

ACTION_SKIP_BACKWARD -> {
val body = HashMap<String, Any>().apply { put("value", 15) }
audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_BACKWARD.ordinal, body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ public NativeAudioAPIModuleSpec(ReactApplicationContext reactContext) {
@DoNotStrip
public abstract void getDevicesInfo(Promise promise);

@ReactMethod
@DoNotStrip
public abstract void setInputDevice(String deviceId, Promise promise);

@ReactMethod
@DoNotStrip
public abstract void showNotification(String type, String key, ReadableMap options, Promise promise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ - (dispatch_queue_t)methodQueue
});
}

RCT_EXPORT_METHOD(
setInputDevice : (NSString *)deviceId resolve : (RCTPromiseResolveBlock)
resolve reject : (RCTPromiseRejectBlock)reject)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.audioSessionManager setInputDevice:deviceId resolve:resolve reject:reject];
});
}

RCT_EXPORT_METHOD(disableSessionManagement)
{
[self.audioSessionManager disableSessionManagement];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@

- (void)getDevicesInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
- (NSArray<NSDictionary *> *)parseDeviceList:(NSArray<AVAudioSessionPortDescription *> *)devices;
- (void)setInputDevice:(NSString *)deviceId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (bool)isSessionActive;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,48 @@ - (void)getDevicesInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectB
[deviceList addObject:@{
@"name" : device.portName,
@"category" : device.portType,
@"uid" : device.UID,
}];
}

return deviceList;
}

- (void)setInputDevice:(NSString *)deviceId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
NSError *error = nil;
NSArray<AVAudioSessionPortDescription *> *availableInputs = [self.audioSession availableInputs];

AVAudioSessionPortDescription *selectedInput = nil;

for (AVAudioSessionPortDescription *input in availableInputs) {
if ([input.UID isEqualToString:deviceId]) {
selectedInput = input;
break;
}
}

if (selectedInput == nil) {
reject(nil, [NSString stringWithFormat:@"Input device with id %@ not found", deviceId], nil);
return;
}

[self.audioSession setPreferredInput:selectedInput error:&error];

if (error != nil) {
reject(
nil,
[NSString
stringWithFormat:@"Error while setting preferred input: %@", [error debugDescription]],
error);
return;
}

resolve(@(true));
}

- (AVAudioSessionCategory)categoryFromString:(NSString *)categorySTR
{
AVAudioSessionCategory category = 0;
Expand Down Expand Up @@ -369,8 +405,8 @@ - (AVAudioSessionCategoryOptions)optionsFromArray:(NSArray *)optionsArray
options |= AVAudioSessionCategoryOptionMixWithOthers;
}

if ([option isEqualToString:@"allowBluetooth"]) {
options |= AVAudioSessionCategoryOptionAllowBluetooth;
if ([option isEqualToString:@"allowBluetoothHFP"]) {
options |= AVAudioSessionCategoryOptionAllowBluetoothHFP;
}

if ([option isEqualToString:@"defaultToSpeaker"]) {
Expand Down
10 changes: 5 additions & 5 deletions packages/react-native-audio-api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { default as DelayNode } from './core/DelayNode';
export { default as GainNode } from './core/GainNode';
export { default as OfflineAudioContext } from './core/OfflineAudioContext';
export { default as OscillatorNode } from './core/OscillatorNode';
export { default as PeriodicWave } from './core/PeriodicWave';
export { default as RecorderAdapterNode } from './core/RecorderAdapterNode';
export { default as StereoPannerNode } from './core/StereoPannerNode';
export { default as StreamerNode } from './core/StreamerNode';
Expand All @@ -28,17 +29,16 @@ export { default as WorkletNode } from './core/WorkletNode';
export { default as WorkletProcessingNode } from './core/WorkletProcessingNode';
export { default as WorkletSourceNode } from './core/WorkletSourceNode';

export { default as useSystemVolume } from './hooks/useSystemVolume';
export { default as PeriodicWave } from './core/PeriodicWave';

export * from './errors';
export * from './system/types';
export * from './types';
export { default as FilePreset } from './utils/filePresets';

// Notification System
export { PlaybackNotificationManager } from './system/notification';
export { RecordingNotificationManager } from './system/notification';
export {
PlaybackNotificationManager,
RecordingNotificationManager,
} from './system/notification';

export { default as AudioManager } from './system';

Expand Down
28 changes: 16 additions & 12 deletions packages/react-native-audio-api/src/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@ export interface EventTypeWithValue {
value: number;
}

interface OnInterruptionEventType {
type: 'ended' | 'began';
export type InterruptionType = 'began' | 'ended';

export interface OnInterruptionEventType {
type: InterruptionType;
shouldResume: boolean;
}

interface OnRouteChangeEventType {
reason:
| 'Unknown'
| 'Override'
| 'CategoryChange'
| 'WakeFromSleep'
| 'NewDeviceAvailable'
| 'OldDeviceUnavailable'
| 'ConfigurationChange'
| 'NoSuitableRouteForCategory';
export type RouteChangeReason =
| 'Unknown'
| 'Override'
| 'CategoryChange'
| 'WakeFromSleep'
| 'NewDeviceAvailable'
| 'OldDeviceUnavailable'
| 'ConfigurationChange'
| 'NoSuitableRouteForCategory';

export interface OnRouteChangeEventType {
reason: RouteChangeReason;
}

export interface OnRecorderErrorEventType {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-audio-api/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as useAudioInput } from './useAudioInput';
export { default as useSystemVolume } from './useSystemVolume';
Loading