From 6c12681b9cfe8327905ab8ab66f58145869f272f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Wed, 18 Jan 2017 13:30:11 -0600 Subject: [PATCH] [RN][iOS] Default to speaker for video conferences --- ios/app/AudioMode.h | 14 ++ ios/app/AudioMode.m | 130 ++++++++++++++++++ .../project.pbxproj | 6 + react/features/app/components/App.native.js | 1 + react/features/app/index.js | 1 + react/features/audio-mode/index.js | 1 + react/features/audio-mode/middleware.js | 56 ++++++++ react/features/base/react-native/AudioMode.js | 20 +++ react/features/base/react-native/index.js | 1 + 9 files changed, 230 insertions(+) create mode 100644 ios/app/AudioMode.h create mode 100644 ios/app/AudioMode.m create mode 100644 react/features/audio-mode/index.js create mode 100644 react/features/audio-mode/middleware.js create mode 100644 react/features/base/react-native/AudioMode.js diff --git a/ios/app/AudioMode.h b/ios/app/AudioMode.h new file mode 100644 index 000000000..30ad863a2 --- /dev/null +++ b/ios/app/AudioMode.h @@ -0,0 +1,14 @@ +#import +#import + +#import "RCTBridgeModule.h" + + +@interface AudioMode : NSObject + +@property (nonatomic, readonly) AVAudioSession *session; +@property (nonatomic, readonly) NSString *category; +@property (nonatomic, readonly) NSString *mode; +@property (nonatomic, readonly) BOOL initialized; + +@end diff --git a/ios/app/AudioMode.m b/ios/app/AudioMode.m new file mode 100644 index 000000000..1ad5e482d --- /dev/null +++ b/ios/app/AudioMode.m @@ -0,0 +1,130 @@ +#import "AudioMode.h" +#import "RCTLog.h" + + +@implementation AudioMode + +RCT_EXPORT_MODULE(); + +typedef enum { + kAudioModeDefault, + kAudioModeAudioCall, + kAudioModeVideoCall +} JitsiMeetAudioMode; + +- (instancetype)init +{ + self = [super init]; + if (self) { + _initialized = NO; + _category = nil; + _mode = nil; + _session = [AVAudioSession sharedInstance]; + } + return self; +} + +- (dispatch_queue_t)methodQueue +{ + // Make sure all our methods run in the main thread. The route change + // notification runs there so this will make sure it will only be fired + // after our changes have been applied (when we cause them, that is). + return dispatch_get_main_queue(); +} + +- (void)routeChanged:(NSNotification*)notification { + NSDictionary *dict = notification.userInfo; + NSInteger reason = [[dict valueForKey:AVAudioSessionRouteChangeReasonKey] + integerValue]; + switch (reason) { + case AVAudioSessionRouteChangeReasonCategoryChange: { + // The category has changed, check if it's the one we want and adjust + // as needed. + BOOL success; + NSError *error; + + if (_session.category != _category) { + success = [_session setCategory: _category error: &error]; + if (!success || error) { + RCTLogInfo(@"Error overriding the desired session category"); + } + } + + if (_session.mode != _mode) { + success = [_session setMode: _mode error: &error]; + if (!success || error) { + RCTLogInfo(@"Error overriding the desired session mode"); + } + } + } + default: + // Do nothing + break; + } +} + +- (NSDictionary *)constantsToExport +{ + return @{ @"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall], + @"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall], + @"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault] + }; +}; + +RCT_EXPORT_METHOD(setMode:(int)mode + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSError *error; + BOOL success; + NSString *avCategory; + NSString *avMode; + + switch (mode) { + case kAudioModeAudioCall: + avCategory = AVAudioSessionCategoryPlayAndRecord; + avMode = AVAudioSessionModeVoiceChat; + break; + case kAudioModeVideoCall: + avCategory = AVAudioSessionCategoryPlayAndRecord; + avMode = AVAudioSessionModeVideoChat; + break; + case kAudioModeDefault: + avCategory = AVAudioSessionCategorySoloAmbient; + avMode = AVAudioSessionModeDefault; + break; + default: + reject(@"setMode", @"Invalid mode", nil); + return; + } + + // Configure AVAudioSession category + success = [_session setCategory: avCategory error: &error]; + if (!success || error) { + reject(@"setMode", error.localizedDescription, error); + return; + } + + // Configure AVAudioSession mode + success = [_session setMode: avMode error: &error]; + if (!success || error) { + reject(@"setMode", error.localizedDescription, error); + return; + } + + // Save the desired mode and category + _category = avCategory; + _mode = avMode; + + // Initialize audio route changes observer if needed + if (!_initialized) { + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(routeChanged:) + name: AVAudioSessionRouteChangeNotification + object: nil]; + _initialized = YES; + } + + resolve(nil); +} + +@end diff --git a/ios/jitsi-meet-react.xcodeproj/project.pbxproj b/ios/jitsi-meet-react.xcodeproj/project.pbxproj index a8628ed5c..937237c99 100644 --- a/ios/jitsi-meet-react.xcodeproj/project.pbxproj +++ b/ios/jitsi-meet-react.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; + 0B42DFAE1E2FD90700111B12 /* AudioMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B42DFAD1E2FD90700111B12 /* AudioMode.m */; }; 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; @@ -205,6 +206,8 @@ 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = "../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj"; sourceTree = ""; }; 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = ""; }; + 0B42DFAC1E2FD90700111B12 /* AudioMode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AudioMode.h; path = app/AudioMode.h; sourceTree = ""; }; + 0B42DFAD1E2FD90700111B12 /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AudioMode.m; path = app/AudioMode.m; sourceTree = ""; }; 0EA8C046B2BF46279796F07D /* libKCKeepAwake.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libKCKeepAwake.a; sourceTree = ""; }; 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; @@ -348,6 +351,8 @@ children = ( 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 0B42DFAC1E2FD90700111B12 /* AudioMode.h */, + 0B42DFAD1E2FD90700111B12 /* AudioMode.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, @@ -778,6 +783,7 @@ buildActionMask = 2147483647; files = ( B3A9D0251E0481E10009343D /* POSIX.m in Sources */, + 0B42DFAE1E2FD90700111B12 /* AudioMode.m in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index 5de51b59e..d316796a2 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -3,6 +3,7 @@ import { Linking } from 'react-native'; import { Platform } from '../../base/react'; +import '../../audio-mode'; import '../../wake-lock'; import { AbstractApp } from './AbstractApp'; diff --git a/react/features/app/index.js b/react/features/app/index.js index cc25e46aa..b845e8859 100644 --- a/react/features/app/index.js +++ b/react/features/app/index.js @@ -1,3 +1,4 @@ export * from './actions'; +export * from './actionTypes'; export * from './components'; export * from './functions'; diff --git a/react/features/audio-mode/index.js b/react/features/audio-mode/index.js new file mode 100644 index 000000000..d43689289 --- /dev/null +++ b/react/features/audio-mode/index.js @@ -0,0 +1 @@ +import './middleware'; diff --git a/react/features/audio-mode/middleware.js b/react/features/audio-mode/middleware.js new file mode 100644 index 000000000..5d3b9df50 --- /dev/null +++ b/react/features/audio-mode/middleware.js @@ -0,0 +1,56 @@ +import { AudioMode } from '../base/react-native'; + +import { APP_WILL_MOUNT } from '../app'; +import { + CONFERENCE_FAILED, + CONFERENCE_LEFT, + CONFERENCE_WILL_JOIN +} from '../base/conference'; + +import { MiddlewareRegistry } from '../base/redux'; + +/** + * Middleware that captures conference actions and sets the correct audio + * mode based on the type of conference. Audio-only conferences don't + * use the speaker by default, and video conferences do. + * + * @param {Store} store - Redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case APP_WILL_MOUNT: { + AudioMode.setMode(AudioMode.DEFAULT) + .catch(err => { + console.warn(`Error setting audio mode: ${err}`); + }); + break; + } + case CONFERENCE_WILL_JOIN: { + let mode; + const state = store.getState()['features/base/conference']; + + if (state.audioOnly) { + // TODO(saghul): Implement audio-only mode + mode = AudioMode.AUDIO_CALL; + } else { + mode = AudioMode.VIDEO_CALL; + } + + AudioMode.setMode(mode) + .catch(err => { + console.warn(`Error setting audio mode: ${err}`); + }); + break; + } + case CONFERENCE_FAILED: + case CONFERENCE_LEFT: + AudioMode.setMode(AudioMode.DEFAULT) + .catch(err => { + console.warn(`Error setting audio mode: ${err}`); + }); + break; + } + + return next(action); +}); diff --git a/react/features/base/react-native/AudioMode.js b/react/features/base/react-native/AudioMode.js new file mode 100644 index 000000000..3a4bb6c48 --- /dev/null +++ b/react/features/base/react-native/AudioMode.js @@ -0,0 +1,20 @@ +import { NativeModules } from 'react-native'; +import { Platform } from '../react'; + +let AudioMode; + +if (Platform.OS === 'ios') { + AudioMode = NativeModules.AudioMode; +} else { + // TODO(saghul): Implement for Android + AudioMode = { + DEFAULT: 0, + AUDIO_CALL: 1, + VIDEO_CALL: 2, + setMode() { + return Promise.resolve(null); + } + }; +} + +export default AudioMode; diff --git a/react/features/base/react-native/index.js b/react/features/base/react-native/index.js index 0690ec17f..0e27480f9 100644 --- a/react/features/base/react-native/index.js +++ b/react/features/base/react-native/index.js @@ -1 +1,2 @@ +export { default as AudioMode } from './AudioMode'; export { default as POSIX } from './POSIX';