diff --git a/example/src/RoomPage.tsx b/example/src/RoomPage.tsx index 3f8d296..4bb4b72 100644 --- a/example/src/RoomPage.tsx +++ b/example/src/RoomPage.tsx @@ -27,6 +27,7 @@ import { type TrackReferenceOrPlaceholder, type ReceivedDataMessage, AndroidAudioTypePresets, + useIOSAudioManagement, } from '@livekit/react-native'; import { Platform } from 'react-native'; // @ts-ignore @@ -89,6 +90,7 @@ const RoomView = ({ navigation }: RoomViewProps) => { const [isCameraFrontFacing, setCameraFrontFacing] = useState(true); const room = useRoomContext(); + useIOSAudioManagement(room, true); // Setup room listeners const { send } = useDataChannel( (dataMessage: ReceivedDataMessage) => { diff --git a/ios/LivekitReactNative.m b/ios/LivekitReactNative.m index 51e5574..ee9600e 100644 --- a/ios/LivekitReactNative.m +++ b/ios/LivekitReactNative.m @@ -121,44 +121,59 @@ +(void)setup { /// Configure audio config for WebRTC RCT_EXPORT_METHOD(setAppleAudioConfiguration:(NSDictionary *) configuration){ - RTCAudioSession* session = [RTCAudioSession sharedInstance]; - RTCAudioSessionConfiguration* config = [RTCAudioSessionConfiguration webRTCConfiguration]; - - NSString* appleAudioCategory = configuration[@"audioCategory"]; - NSArray* appleAudioCategoryOptions = configuration[@"audioCategoryOptions"]; - NSString* appleAudioMode = configuration[@"audioMode"]; - - [session lockForConfiguration]; - - if(appleAudioCategoryOptions != nil) { - config.categoryOptions = 0; - for(NSString* option in appleAudioCategoryOptions) { - if([@"mixWithOthers" isEqualToString:option]) { - config.categoryOptions |= AVAudioSessionCategoryOptionMixWithOthers; - } else if([@"duckOthers" isEqualToString:option]) { - config.categoryOptions |= AVAudioSessionCategoryOptionDuckOthers; - } else if([@"allowBluetooth" isEqualToString:option]) { - config.categoryOptions |= AVAudioSessionCategoryOptionAllowBluetooth; - } else if([@"allowBluetoothA2DP" isEqualToString:option]) { - config.categoryOptions |= AVAudioSessionCategoryOptionAllowBluetoothA2DP; - } else if([@"allowAirPlay" isEqualToString:option]) { - config.categoryOptions |= AVAudioSessionCategoryOptionAllowAirPlay; - } else if([@"defaultToSpeaker" isEqualToString:option]) { - config.categoryOptions |= AVAudioSessionCategoryOptionDefaultToSpeaker; - } + RTCAudioSession* session = [RTCAudioSession sharedInstance]; + RTCAudioSessionConfiguration* config = [RTCAudioSessionConfiguration webRTCConfiguration]; + + NSString* appleAudioCategory = configuration[@"audioCategory"]; + NSArray* appleAudioCategoryOptions = configuration[@"audioCategoryOptions"]; + NSString* appleAudioMode = configuration[@"audioMode"]; + + [session lockForConfiguration]; + + NSError* error = nil; + BOOL categoryChanged = NO; + if(appleAudioCategoryOptions != nil) { + categoryChanged = YES; + config.categoryOptions = 0; + for(NSString* option in appleAudioCategoryOptions) { + if([@"mixWithOthers" isEqualToString:option]) { + config.categoryOptions |= AVAudioSessionCategoryOptionMixWithOthers; + } else if([@"duckOthers" isEqualToString:option]) { + config.categoryOptions |= AVAudioSessionCategoryOptionDuckOthers; + } else if([@"allowBluetooth" isEqualToString:option]) { + config.categoryOptions |= AVAudioSessionCategoryOptionAllowBluetooth; + } else if([@"allowBluetoothA2DP" isEqualToString:option]) { + config.categoryOptions |= AVAudioSessionCategoryOptionAllowBluetoothA2DP; + } else if([@"allowAirPlay" isEqualToString:option]) { + config.categoryOptions |= AVAudioSessionCategoryOptionAllowAirPlay; + } else if([@"defaultToSpeaker" isEqualToString:option]) { + config.categoryOptions |= AVAudioSessionCategoryOptionDefaultToSpeaker; + } + } } - } - if(appleAudioCategory != nil) { - config.category = [AudioUtils audioSessionCategoryFromString:appleAudioCategory]; - [session setCategory:config.category withOptions:config.categoryOptions error:nil]; - } + if(appleAudioCategory != nil) { + categoryChanged = YES; + config.category = [AudioUtils audioSessionCategoryFromString:appleAudioCategory]; + } + + if(categoryChanged) { + [session setCategory:config.category withOptions:config.categoryOptions error:&error]; + if(error != nil) { + NSLog(@"Error setting category: %@", [error localizedDescription]); + error = nil; + } + } - if(appleAudioMode != nil) { - config.mode = [AudioUtils audioSessionModeFromString:appleAudioMode]; - [session setMode:config.mode error:nil]; - } + if(appleAudioMode != nil) { + config.mode = [AudioUtils audioSessionModeFromString:appleAudioMode]; + [session setMode:config.mode error:&error]; + if(error != nil) { + NSLog(@"Error setting category: %@", [error localizedDescription]); + error = nil; + } + } - [session unlockForConfiguration]; + [session unlockForConfiguration]; } @end diff --git a/src/audio/AudioSession.ts b/src/audio/AudioSession.ts index 58005d0..ae36957 100644 --- a/src/audio/AudioSession.ts +++ b/src/audio/AudioSession.ts @@ -325,6 +325,12 @@ export default class AudioSession { } }; + /** + * Directly change the AVAudioSession category/mode. + * + * @param config The configuration to use. Null values will be omitted and the + * existing values will be unchanged. + */ static setAppleAudioConfiguration = async ( config: AppleAudioConfiguration ) => { diff --git a/src/index.tsx b/src/index.tsx index 74522e8..6fdbe5d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,7 +13,7 @@ import AudioSession, { } from './audio/AudioSession'; import type { AudioConfiguration } from './audio/AudioSession'; import { PixelRatio, Platform } from 'react-native'; -import type { LiveKitReactNativeInfo } from 'livekit-client'; +import { type LiveKitReactNativeInfo } from 'livekit-client'; import type { LogLevel, SetLogLevelOptions } from './logger'; /** @@ -23,6 +23,7 @@ import type { LogLevel, SetLogLevelOptions } from './logger'; */ export function registerGlobals() { webrtcRegisterGlobals(); + iosCategoryEnforce(); livekitRegisterGlobals(); setupURLPolyfill(); fixWebrtcAdapter(); @@ -31,6 +32,27 @@ export function registerGlobals() { shimAsyncIterator(); shimIterator(); } + +/** + * Enforces changing to playAndRecord category prior to obtaining microphone. + */ +function iosCategoryEnforce() { + if (Platform.OS === 'ios') { + // @ts-ignore + let getUserMediaFunc = global.navigator.mediaDevices.getUserMedia; + // @ts-ignore + global.navigator.mediaDevices.getUserMedia = async (constraints: any) => { + if (constraints.audio) { + await AudioSession.setAppleAudioConfiguration({ + audioCategory: 'playAndRecord', + }); + } + + return await getUserMediaFunc(constraints); + }; + } +} + function livekitRegisterGlobals() { let lkGlobal: LiveKitReactNativeInfo = { platform: Platform.OS,