diff --git a/ios/app/AudioMode.h b/ios/app/AudioMode.h deleted file mode 100644 index 30ad863a2..000000000 --- a/ios/app/AudioMode.h +++ /dev/null @@ -1,14 +0,0 @@ -#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 index 1ad5e482d..4acec567a 100644 --- a/ios/app/AudioMode.m +++ b/ios/app/AudioMode.m @@ -1,8 +1,15 @@ -#import "AudioMode.h" +#import "RCTBridgeModule.h" #import "RCTLog.h" +#import -@implementation AudioMode +@interface AudioMode : NSObject +@end + +@implementation AudioMode { + NSString *_category; + NSString *_mode; +} RCT_EXPORT_MODULE(); @@ -12,20 +19,24 @@ typedef enum { kAudioModeVideoCall } JitsiMeetAudioMode; -- (instancetype)init -{ +- (NSDictionary *)constantsToExport { + return @{ + @"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall], + @"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault], + @"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall] + }; +}; + +- (instancetype)init { self = [super init]; if (self) { - _initialized = NO; _category = nil; _mode = nil; - _session = [AVAudioSession sharedInstance]; } return self; } -- (dispatch_queue_t)methodQueue -{ +- (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). @@ -33,97 +44,90 @@ typedef enum { } - (void)routeChanged:(NSNotification*)notification { - NSDictionary *dict = notification.userInfo; - NSInteger reason = [[dict valueForKey:AVAudioSessionRouteChangeReasonKey] - integerValue]; + NSInteger reason + = [[notification.userInfo + 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; + case AVAudioSessionRouteChangeReasonCategoryChange: + // The category has changed. Check if it's the one we want and adjust as + // needed. + [self setCategory:_category mode:_mode error:nil]; + break; - 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 + // Do nothing. break; } } -- (NSDictionary *)constantsToExport -{ - return @{ @"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall], - @"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall], - @"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault] - }; -}; +- (BOOL)setCategory:(NSString *)category + mode:(NSString *)mode + error:(NSError * _Nullable *)outError { + AVAudioSession *session = [AVAudioSession sharedInstance]; + + if (session.category != category + && ![session setCategory:category error:outError]) { + RCTLogError(@"Failed to (re)apply specified AVAudioSession category!"); + return NO; + } + + if (session.mode != mode && ![session setMode:mode error:outError]) { + RCTLogError(@"Failed to (re)apply specified AVAudioSession mode!"); + return NO; + } + + return YES; +} RCT_EXPORT_METHOD(setMode:(int)mode resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - NSError *error; - BOOL success; NSString *avCategory; NSString *avMode; + NSError *error; switch (mode) { case kAudioModeAudioCall: avCategory = AVAudioSessionCategoryPlayAndRecord; avMode = AVAudioSessionModeVoiceChat; break; - case kAudioModeVideoCall: - avCategory = AVAudioSessionCategoryPlayAndRecord; - avMode = AVAudioSessionModeVideoChat; - break; case kAudioModeDefault: avCategory = AVAudioSessionCategorySoloAmbient; avMode = AVAudioSessionModeDefault; break; + case kAudioModeVideoCall: + avCategory = AVAudioSessionCategoryPlayAndRecord; + avMode = AVAudioSessionModeVideoChat; + break; default: reject(@"setMode", @"Invalid mode", nil); return; } - // Configure AVAudioSession category - success = [_session setCategory: avCategory error: &error]; - if (!success || error) { + if (![self setCategory:avCategory mode:avMode error:&error] || 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; + // Even though the specified category and mode were successfully set, the + // AVAudioSession is a singleton and other parts of the application such as + // WebRTC may undo the settings. Make sure that the settings are reapplied + // upon undoes. + if (!_category || !_mode) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(routeChanged:) + name:AVAudioSessionRouteChangeNotification + object:nil]; } - // Save the desired mode and category + // Save the desired/specified category and mode so that they may be + // reapplied (upon undoes as described above). _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); } diff --git a/ios/app/POSIX.h b/ios/app/POSIX.h deleted file mode 100644 index 1f589fb55..000000000 --- a/ios/app/POSIX.h +++ /dev/null @@ -1,4 +0,0 @@ -#import "RCTBridgeModule.h" - -@interface POSIX : NSObject -@end diff --git a/ios/app/POSIX.m b/ios/app/POSIX.m index 8f6af0ff7..19a5a3713 100644 --- a/ios/app/POSIX.m +++ b/ios/app/POSIX.m @@ -1,10 +1,13 @@ -#import "POSIX.h" +#import "RCTBridgeModule.h" #include #include #include #include +@interface POSIX : NSObject +@end + @implementation POSIX RCT_EXPORT_MODULE(); diff --git a/ios/jitsi-meet-react.xcodeproj/project.pbxproj b/ios/jitsi-meet-react.xcodeproj/project.pbxproj index 937237c99..ee33635ac 100644 --- a/ios/jitsi-meet-react.xcodeproj/project.pbxproj +++ b/ios/jitsi-meet-react.xcodeproj/project.pbxproj @@ -206,7 +206,6 @@ 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 = ""; }; @@ -226,7 +225,6 @@ 821D8ABD506944B4BDBB069B /* libRNVectorIcons.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNVectorIcons.a; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; B30EF2301DC0ED7C00690F45 /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = ""; }; - B3A9D0231E0481E10009343D /* POSIX.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = POSIX.h; path = app/POSIX.h; sourceTree = ""; }; B3A9D0241E0481E10009343D /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = POSIX.m; path = app/POSIX.m; sourceTree = ""; }; B3B083EB1D4955FF0069CEE7 /* jitsi-meet-react.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "jitsi-meet-react.entitlements"; sourceTree = ""; }; B96AF9B6FBC0453798399985 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; }; @@ -351,14 +349,12 @@ children = ( 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, - 0B42DFAC1E2FD90700111B12 /* AudioMode.h */, 0B42DFAD1E2FD90700111B12 /* AudioMode.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FB71A68108700A75B9A /* main.m */, - B3A9D0231E0481E10009343D /* POSIX.h */, B3A9D0241E0481E10009343D /* POSIX.m */, ); name = app; @@ -402,6 +398,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 5B09C20C78C74A548AAAC1FA /* KCKeepAwake.xcodeproj */, BF96438F1C34FBEB00B0BBDF /* libc.tbd */, BF9643911C34FBF100B0BBDF /* libsqlite3.tbd */, BF9643931C34FBF900B0BBDF /* libstdc++.tbd */, @@ -417,7 +414,6 @@ 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, 22418656B14845609F953A42 /* RNVectorIcons.xcodeproj */, - 5B09C20C78C74A548AAAC1FA /* KCKeepAwake.xcodeproj */, ); name = Libraries; sourceTree = ""; diff --git a/react/features/audio-mode/middleware.js b/react/features/audio-mode/middleware.js index 5d3b9df50..716185623 100644 --- a/react/features/audio-mode/middleware.js +++ b/react/features/audio-mode/middleware.js @@ -1,4 +1,4 @@ -import { AudioMode } from '../base/react-native'; +import { NativeModules } from 'react-native'; import { APP_WILL_MOUNT } from '../app'; import { @@ -6,50 +6,51 @@ import { 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. + * 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']; + const AudioMode = NativeModules.AudioMode; - if (state.audioOnly) { - // TODO(saghul): Implement audio-only mode - mode = AudioMode.AUDIO_CALL; - } else { - mode = AudioMode.VIDEO_CALL; + // The react-native module AudioMode is implemented on iOS at the time of + // this writing. + if (AudioMode) { + let audioMode; + + switch (action.type) { + case APP_WILL_MOUNT: + case CONFERENCE_FAILED: + case CONFERENCE_LEFT: + audioMode = AudioMode.DEFAULT; + break; + + case CONFERENCE_WILL_JOIN: { + const conference = store.getState()['features/base/conference']; + + audioMode + = conference.audioOnly + ? AudioMode.AUDIO_CALL + : AudioMode.VIDEO_CALL; + break; } - AudioMode.setMode(mode) - .catch(err => { - console.warn(`Error setting audio mode: ${err}`); + default: + audioMode = null; + break; + } + + if (audioMode !== null) { + AudioMode.setMode(audioMode).catch(err => { + console.error(`Failed to set audio mode ${audioMode}: ${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/conference/actionTypes.js b/react/features/base/conference/actionTypes.js index 2624c341e..69c622977 100644 --- a/react/features/base/conference/actionTypes.js +++ b/react/features/base/conference/actionTypes.js @@ -23,17 +23,6 @@ export const CONFERENCE_FAILED = Symbol('CONFERENCE_FAILED'); */ export const CONFERENCE_JOINED = Symbol('CONFERENCE_JOINED'); -/** - * The type of the Redux action which signals that a specific conference will be - * joined. - * - * { - * type: CONFERENCE_WILL_JOIN, - * room: string - * } - */ -export const CONFERENCE_WILL_JOIN = Symbol('CONFERENCE_WILL_JOIN'); - /** * The type of the Redux action which signals that a specific conference has * been left. @@ -45,6 +34,17 @@ export const CONFERENCE_WILL_JOIN = Symbol('CONFERENCE_WILL_JOIN'); */ export const CONFERENCE_LEFT = Symbol('CONFERENCE_LEFT'); +/** + * The type of the Redux action which signals that a specific conference will be + * joined. + * + * { + * type: CONFERENCE_WILL_JOIN, + * room: string + * } + */ +export const CONFERENCE_WILL_JOIN = Symbol('CONFERENCE_WILL_JOIN'); + /** * The type of the Redux action which signals that a specific conference will be * left. diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index 39547ee46..bb2102267 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -124,26 +124,6 @@ function _conferenceJoined(conference) { }; } -/** - * Signals the intention of the application to have the local participant leave - * a specific conference. Similar in fashion to CONFERENCE_LEFT. Contrary to it - * though, it's not guaranteed because CONFERENCE_LEFT may be triggered by - * lib-jitsi-meet and not the application. - * - * @param {string} room - The JitsiConference instance which will - * be left by the local participant. - * @returns {{ - * type: CONFERENCE_WILL_JOIN, - * room: string - * }} - */ -function _conferenceWillJoin(room) { - return { - type: CONFERENCE_WILL_JOIN, - room - }; -} - /** * Signals that a specific conference has been left. * @@ -161,6 +141,25 @@ function _conferenceLeft(conference) { }; } +/** + * Signals the intention of the application to have the local participant join a + * conference with a specific room (name). Similar in fashion + * to CONFERENCE_JOINED. + * + * @param {string} room - The room (name) which identifies the conference the + * local participant will (try to) join. + * @returns {{ + * type: CONFERENCE_WILL_JOIN, + * room: string + * }} + */ +function _conferenceWillJoin(room) { + return { + type: CONFERENCE_WILL_JOIN, + room + }; +} + /** * Signals the intention of the application to have the local participant leave * a specific conference. Similar in fashion to CONFERENCE_LEFT. Contrary to it @@ -201,15 +200,14 @@ export function createConference() { throw new Error('Cannot join conference without room name'); } - // XXX Lib-jitsi-meet does not accept uppercase letters. - const _room = room.toLowerCase(); - - dispatch(_conferenceWillJoin(_room)); + dispatch(_conferenceWillJoin(room)); // TODO Take options from config. const conference = connection.initJitsiConference( - _room, + + // XXX Lib-jitsi-meet does not accept uppercase letters. + room.toLowerCase(), { openSctp: true }); _addConferenceListeners(conference, dispatch); diff --git a/react/features/base/react-native/AudioMode.js b/react/features/base/react-native/AudioMode.js deleted file mode 100644 index 3a4bb6c48..000000000 --- a/react/features/base/react-native/AudioMode.js +++ /dev/null @@ -1,20 +0,0 @@ -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 0e27480f9..0690ec17f 100644 --- a/react/features/base/react-native/index.js +++ b/react/features/base/react-native/index.js @@ -1,2 +1 @@ -export { default as AudioMode } from './AudioMode'; export { default as POSIX } from './POSIX';