2017-05-08 15:21:30 +00:00
|
|
|
/*
|
2019-08-09 10:41:52 +00:00
|
|
|
* Copyright @ 2017-present 8x8, Inc.
|
2017-05-08 15:21:30 +00:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2017-01-20 18:11:11 +00:00
|
|
|
#import <AVFoundation/AVFoundation.h>
|
2017-01-18 19:30:11 +00:00
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
#import <React/RCTEventEmitter.h>
|
2017-10-18 16:04:31 +00:00
|
|
|
#import <React/RCTLog.h>
|
2019-02-13 15:55:51 +00:00
|
|
|
#import <WebRTC/WebRTC.h>
|
2017-10-18 16:04:31 +00:00
|
|
|
|
2021-10-22 08:30:25 +00:00
|
|
|
#import "JitsiAudioSession+Private.h"
|
2019-08-28 10:31:38 +00:00
|
|
|
#import "LogUtils.h"
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
|
|
|
|
// Audio mode
|
2019-02-05 13:49:35 +00:00
|
|
|
typedef enum {
|
|
|
|
kAudioModeDefault,
|
|
|
|
kAudioModeAudioCall,
|
|
|
|
kAudioModeVideoCall
|
|
|
|
} JitsiMeetAudioMode;
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
// Events
|
|
|
|
static NSString * const kDevicesChanged = @"org.jitsi.meet:features/audio-mode#devices-update";
|
|
|
|
|
|
|
|
// Device types (must match JS and Java)
|
|
|
|
static NSString * const kDeviceTypeBluetooth = @"BLUETOOTH";
|
2022-04-07 14:45:01 +00:00
|
|
|
static NSString * const kDeviceTypeCar = @"CAR";
|
2019-08-09 10:41:52 +00:00
|
|
|
static NSString * const kDeviceTypeEarpiece = @"EARPIECE";
|
2022-04-07 14:45:01 +00:00
|
|
|
static NSString * const kDeviceTypeHeadphones = @"HEADPHONES";
|
2019-08-09 10:41:52 +00:00
|
|
|
static NSString * const kDeviceTypeSpeaker = @"SPEAKER";
|
|
|
|
static NSString * const kDeviceTypeUnknown = @"UNKNOWN";
|
|
|
|
|
|
|
|
|
|
|
|
@interface AudioMode : RCTEventEmitter<RTCAudioSessionDelegate>
|
2018-07-26 13:54:56 +00:00
|
|
|
|
|
|
|
@property(nonatomic, strong) dispatch_queue_t workerQueue;
|
|
|
|
|
2017-01-20 18:11:11 +00:00
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation AudioMode {
|
2019-02-13 15:55:51 +00:00
|
|
|
JitsiMeetAudioMode activeMode;
|
|
|
|
RTCAudioSessionConfiguration *defaultConfig;
|
|
|
|
RTCAudioSessionConfiguration *audioCallConfig;
|
|
|
|
RTCAudioSessionConfiguration *videoCallConfig;
|
2019-08-09 10:41:52 +00:00
|
|
|
RTCAudioSessionConfiguration *earpieceConfig;
|
|
|
|
BOOL forceSpeaker;
|
|
|
|
BOOL forceEarpiece;
|
|
|
|
BOOL isSpeakerOn;
|
|
|
|
BOOL isEarpieceOn;
|
2017-01-20 18:11:11 +00:00
|
|
|
}
|
2017-01-18 19:30:11 +00:00
|
|
|
|
|
|
|
RCT_EXPORT_MODULE();
|
|
|
|
|
2018-05-21 10:35:59 +00:00
|
|
|
+ (BOOL)requiresMainQueueSetup {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
- (NSArray<NSString *> *)supportedEvents {
|
|
|
|
return @[ kDevicesChanged ];
|
|
|
|
}
|
|
|
|
|
2017-01-20 18:11:11 +00:00
|
|
|
- (NSDictionary *)constantsToExport {
|
|
|
|
return @{
|
2019-08-09 10:41:52 +00:00
|
|
|
@"DEVICE_CHANGE_EVENT": kDevicesChanged,
|
2017-01-20 18:11:11 +00:00
|
|
|
@"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall],
|
|
|
|
@"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault],
|
|
|
|
@"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall]
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
- (instancetype)init {
|
2017-01-18 19:30:11 +00:00
|
|
|
self = [super init];
|
|
|
|
if (self) {
|
2018-07-26 13:54:56 +00:00
|
|
|
dispatch_queue_attr_t attributes =
|
2019-08-09 10:41:52 +00:00
|
|
|
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
|
2018-12-18 09:15:21 +00:00
|
|
|
_workerQueue = dispatch_queue_create("AudioMode.queue", attributes);
|
2019-02-05 13:49:35 +00:00
|
|
|
|
2019-02-13 15:55:51 +00:00
|
|
|
activeMode = kAudioModeDefault;
|
|
|
|
|
|
|
|
defaultConfig = [[RTCAudioSessionConfiguration alloc] init];
|
|
|
|
defaultConfig.category = AVAudioSessionCategoryAmbient;
|
|
|
|
defaultConfig.categoryOptions = 0;
|
|
|
|
defaultConfig.mode = AVAudioSessionModeDefault;
|
|
|
|
|
|
|
|
audioCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
|
|
|
audioCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
|
2019-08-09 10:41:52 +00:00
|
|
|
audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker;
|
2019-02-13 15:55:51 +00:00
|
|
|
audioCallConfig.mode = AVAudioSessionModeVoiceChat;
|
|
|
|
|
|
|
|
videoCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
|
|
|
videoCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
|
|
|
|
videoCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
|
|
|
|
videoCallConfig.mode = AVAudioSessionModeVideoChat;
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
// Manually routing audio to the earpiece doesn't quite work unless one disables BT (weird, I know).
|
|
|
|
earpieceConfig = [[RTCAudioSessionConfiguration alloc] init];
|
|
|
|
earpieceConfig.category = AVAudioSessionCategoryPlayAndRecord;
|
|
|
|
earpieceConfig.categoryOptions = 0;
|
|
|
|
earpieceConfig.mode = AVAudioSessionModeVoiceChat;
|
|
|
|
|
|
|
|
forceSpeaker = NO;
|
|
|
|
forceEarpiece = NO;
|
|
|
|
isSpeakerOn = NO;
|
|
|
|
isEarpieceOn = NO;
|
|
|
|
|
2021-10-22 08:30:25 +00:00
|
|
|
RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
|
2019-02-13 15:55:51 +00:00
|
|
|
[session addDelegate:self];
|
2017-01-18 19:30:11 +00:00
|
|
|
}
|
2019-02-13 15:55:51 +00:00
|
|
|
|
2017-01-18 19:30:11 +00:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2017-01-20 18:11:11 +00:00
|
|
|
- (dispatch_queue_t)methodQueue {
|
2018-07-26 13:54:56 +00:00
|
|
|
// Use a dedicated queue for audio mode operations.
|
|
|
|
return _workerQueue;
|
2017-01-18 19:30:11 +00:00
|
|
|
}
|
|
|
|
|
2021-08-19 08:34:54 +00:00
|
|
|
- (BOOL)setConfigWithoutLock:(RTCAudioSessionConfiguration *)config
|
|
|
|
error:(NSError * _Nullable *)outError {
|
2021-10-22 08:30:25 +00:00
|
|
|
RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
|
2021-08-19 08:34:54 +00:00
|
|
|
|
|
|
|
return [session setConfiguration:config error:outError];
|
|
|
|
}
|
|
|
|
|
2019-02-13 15:55:51 +00:00
|
|
|
- (BOOL)setConfig:(RTCAudioSessionConfiguration *)config
|
|
|
|
error:(NSError * _Nullable *)outError {
|
2017-01-20 18:11:11 +00:00
|
|
|
|
2021-10-22 08:30:25 +00:00
|
|
|
RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
|
2019-02-13 15:55:51 +00:00
|
|
|
[session lockForConfiguration];
|
2021-08-19 08:34:54 +00:00
|
|
|
BOOL success = [self setConfigWithoutLock:config error:outError];
|
2019-02-13 15:55:51 +00:00
|
|
|
[session unlockForConfiguration];
|
2017-01-20 18:11:11 +00:00
|
|
|
|
2019-02-13 15:55:51 +00:00
|
|
|
return success;
|
2017-01-20 18:11:11 +00:00
|
|
|
}
|
2017-01-18 19:30:11 +00:00
|
|
|
|
2019-02-13 15:55:51 +00:00
|
|
|
#pragma mark - Exported methods
|
|
|
|
|
2017-01-18 19:30:11 +00:00
|
|
|
RCT_EXPORT_METHOD(setMode:(int)mode
|
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject) {
|
2019-08-09 10:41:52 +00:00
|
|
|
RTCAudioSessionConfiguration *config = [self configForMode:mode];
|
2017-01-20 18:11:11 +00:00
|
|
|
NSError *error;
|
2017-01-18 19:30:11 +00:00
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
if (config == nil) {
|
2017-01-18 19:30:11 +00:00
|
|
|
reject(@"setMode", @"Invalid mode", nil);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
// Reset.
|
|
|
|
if (mode == kAudioModeDefault) {
|
|
|
|
forceSpeaker = NO;
|
|
|
|
forceEarpiece = NO;
|
|
|
|
}
|
|
|
|
|
2019-02-13 15:55:51 +00:00
|
|
|
activeMode = mode;
|
2019-02-05 13:49:35 +00:00
|
|
|
|
2019-02-13 15:55:51 +00:00
|
|
|
if ([self setConfig:config error:&error]) {
|
2019-02-05 13:49:35 +00:00
|
|
|
resolve(nil);
|
2019-02-13 15:55:51 +00:00
|
|
|
} else {
|
|
|
|
reject(@"setMode", error.localizedDescription, error);
|
2017-01-18 19:30:11 +00:00
|
|
|
}
|
2019-08-09 10:41:52 +00:00
|
|
|
|
|
|
|
[self notifyDevicesChanged];
|
|
|
|
}
|
|
|
|
|
|
|
|
RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
|
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject) {
|
2019-08-28 10:31:38 +00:00
|
|
|
DDLogInfo(@"[AudioMode] Selected device: %@", device);
|
2019-08-09 10:41:52 +00:00
|
|
|
|
2021-10-22 08:30:25 +00:00
|
|
|
RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
|
2019-08-09 10:41:52 +00:00
|
|
|
[session lockForConfiguration];
|
|
|
|
BOOL success;
|
|
|
|
NSError *error = nil;
|
|
|
|
|
|
|
|
// Reset these, as we are about to compute them.
|
|
|
|
forceSpeaker = NO;
|
|
|
|
forceEarpiece = NO;
|
|
|
|
|
|
|
|
// The speaker is special, so test for it first.
|
|
|
|
if ([device isEqualToString:kDeviceTypeSpeaker]) {
|
2022-04-08 08:57:05 +00:00
|
|
|
forceSpeaker = YES;
|
2019-08-09 10:41:52 +00:00
|
|
|
success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
|
|
|
|
} else {
|
|
|
|
// Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
|
|
|
|
AVAudioSession *_session = [AVAudioSession sharedInstance];
|
|
|
|
AVAudioSessionPortDescription *port = nil;
|
|
|
|
|
|
|
|
// Find the matching input device.
|
|
|
|
for (AVAudioSessionPortDescription *portDesc in _session.availableInputs) {
|
|
|
|
if ([portDesc.UID isEqualToString:device]) {
|
|
|
|
port = portDesc;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-08-19 08:34:54 +00:00
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
if (port != nil) {
|
|
|
|
// First remove the override if we are going to select a different device.
|
|
|
|
if (isSpeakerOn) {
|
|
|
|
[session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Special case for the earpiece.
|
|
|
|
if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
|
|
|
|
forceEarpiece = YES;
|
2021-08-19 08:34:54 +00:00
|
|
|
[self setConfigWithoutLock:earpieceConfig error:nil];
|
2019-08-09 10:41:52 +00:00
|
|
|
} else if (isEarpieceOn) {
|
|
|
|
// Reset the config.
|
|
|
|
RTCAudioSessionConfiguration *config = [self configForMode:activeMode];
|
2021-08-19 08:34:54 +00:00
|
|
|
[self setConfigWithoutLock:config error:nil];
|
2019-08-09 10:41:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Select our preferred input.
|
|
|
|
success = [session setPreferredInput:port error:&error];
|
|
|
|
} else {
|
|
|
|
success = NO;
|
|
|
|
error = RCTErrorWithMessage(@"Could not find audio device");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[session unlockForConfiguration];
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
resolve(nil);
|
|
|
|
} else {
|
|
|
|
reject(@"setAudioDevice", error != nil ? error.localizedDescription : @"", error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
RCT_EXPORT_METHOD(updateDeviceList) {
|
|
|
|
[self notifyDevicesChanged];
|
2017-01-18 19:30:11 +00:00
|
|
|
}
|
|
|
|
|
2019-02-13 15:55:51 +00:00
|
|
|
#pragma mark - RTCAudioSessionDelegate
|
|
|
|
|
|
|
|
- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
|
|
|
|
reason:(AVAudioSessionRouteChangeReason)reason
|
|
|
|
previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
|
2019-08-09 10:41:52 +00:00
|
|
|
// Update JS about the changes.
|
|
|
|
[self notifyDevicesChanged];
|
|
|
|
|
|
|
|
dispatch_async(_workerQueue, ^{
|
|
|
|
switch (reason) {
|
|
|
|
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
|
|
|
|
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
|
|
|
|
// If the device list changed, reset our overrides.
|
|
|
|
self->forceSpeaker = NO;
|
|
|
|
self->forceEarpiece = NO;
|
|
|
|
break;
|
2022-04-08 08:47:10 +00:00
|
|
|
case AVAudioSessionRouteChangeReasonCategoryChange: {
|
2019-08-09 10:41:52 +00:00
|
|
|
// The category has changed. Check if it's the one we want and adjust as
|
|
|
|
// needed.
|
2022-04-08 08:47:10 +00:00
|
|
|
RTCAudioSessionConfiguration *currentConfig = [self configForMode:self->activeMode];
|
|
|
|
if ([session.category isEqualToString:currentConfig.category]) {
|
|
|
|
// We are in the desired category, nothing to do here.
|
|
|
|
return;
|
|
|
|
}
|
2019-08-09 10:41:52 +00:00
|
|
|
break;
|
2022-04-08 08:47:10 +00:00
|
|
|
}
|
2019-08-09 10:41:52 +00:00
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We don't want to touch the category when in default mode.
|
|
|
|
// This is to play well with other components which could be integrated
|
|
|
|
// into the final application.
|
|
|
|
if (self->activeMode != kAudioModeDefault) {
|
2019-08-28 10:31:38 +00:00
|
|
|
DDLogInfo(@"[AudioMode] Route changed, reapplying RTCAudioSession config");
|
2019-08-09 10:41:52 +00:00
|
|
|
RTCAudioSessionConfiguration *config = [self configForMode:self->activeMode];
|
|
|
|
[self setConfig:config error:nil];
|
|
|
|
if (self->forceSpeaker && !self->isSpeakerOn) {
|
|
|
|
[session lockForConfiguration];
|
|
|
|
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
|
|
|
|
[session unlockForConfiguration];
|
2019-02-13 15:55:51 +00:00
|
|
|
}
|
2019-08-09 10:41:52 +00:00
|
|
|
}
|
|
|
|
});
|
2019-02-13 15:55:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)audioSession:(RTCAudioSession *)audioSession didSetActive:(BOOL)active {
|
2019-08-28 10:31:38 +00:00
|
|
|
DDLogInfo(@"[AudioMode] Audio session didSetActive:%d", active);
|
2019-02-13 15:55:51 +00:00
|
|
|
}
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
#pragma mark - Helper methods
|
|
|
|
|
|
|
|
- (RTCAudioSessionConfiguration *)configForMode:(int) mode {
|
|
|
|
if (mode != kAudioModeDefault && forceEarpiece) {
|
|
|
|
return earpieceConfig;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (mode) {
|
|
|
|
case kAudioModeAudioCall:
|
|
|
|
return audioCallConfig;
|
|
|
|
case kAudioModeDefault:
|
|
|
|
return defaultConfig;
|
|
|
|
case kAudioModeVideoCall:
|
|
|
|
return videoCallConfig;
|
|
|
|
default:
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Here we convert input and output port types into a single type.
|
|
|
|
- (NSString *)portTypeToString:(AVAudioSessionPort) portType {
|
|
|
|
if ([portType isEqualToString:AVAudioSessionPortHeadphones]
|
|
|
|
|| [portType isEqualToString:AVAudioSessionPortHeadsetMic]) {
|
|
|
|
return kDeviceTypeHeadphones;
|
|
|
|
} else if ([portType isEqualToString:AVAudioSessionPortBuiltInMic]
|
|
|
|
|| [portType isEqualToString:AVAudioSessionPortBuiltInReceiver]) {
|
|
|
|
return kDeviceTypeEarpiece;
|
|
|
|
} else if ([portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
|
|
|
|
return kDeviceTypeSpeaker;
|
|
|
|
} else if ([portType isEqualToString:AVAudioSessionPortBluetoothHFP]
|
|
|
|
|| [portType isEqualToString:AVAudioSessionPortBluetoothLE]
|
|
|
|
|| [portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) {
|
|
|
|
return kDeviceTypeBluetooth;
|
2022-04-07 14:45:01 +00:00
|
|
|
} else if ([portType isEqualToString:AVAudioSessionPortCarAudio]) {
|
|
|
|
return kDeviceTypeCar;
|
2019-08-09 10:41:52 +00:00
|
|
|
} else {
|
|
|
|
return kDeviceTypeUnknown;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)notifyDevicesChanged {
|
|
|
|
dispatch_async(_workerQueue, ^{
|
|
|
|
NSMutableArray *data = [[NSMutableArray alloc] init];
|
|
|
|
// Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
|
|
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
|
|
NSString *currentPort = @"";
|
|
|
|
AVAudioSessionRouteDescription *currentRoute = session.currentRoute;
|
|
|
|
|
|
|
|
// Check what the current device is. Because the speaker is somewhat special, we need to
|
|
|
|
// check for it first.
|
|
|
|
if (currentRoute != nil) {
|
|
|
|
AVAudioSessionPortDescription *output = currentRoute.outputs.firstObject;
|
|
|
|
AVAudioSessionPortDescription *input = currentRoute.inputs.firstObject;
|
|
|
|
if (output != nil && [output.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
|
|
|
|
currentPort = kDeviceTypeSpeaker;
|
|
|
|
self->isSpeakerOn = YES;
|
|
|
|
} else if (input != nil) {
|
|
|
|
currentPort = input.UID;
|
|
|
|
self->isSpeakerOn = NO;
|
|
|
|
self->isEarpieceOn = [input.portType isEqualToString:AVAudioSessionPortBuiltInMic];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL headphonesAvailable = NO;
|
|
|
|
for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
|
|
|
|
if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic] || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
|
|
|
|
headphonesAvailable = YES;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-04-08 08:47:10 +00:00
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
|
|
|
|
// Skip "Phone" if headphones are present.
|
|
|
|
if (headphonesAvailable && [portDesc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
id deviceData
|
|
|
|
= @{
|
|
|
|
@"type": [self portTypeToString:portDesc.portType],
|
|
|
|
@"name": portDesc.portName,
|
|
|
|
@"uid": portDesc.UID,
|
|
|
|
@"selected": [NSNumber numberWithBool:[portDesc.UID isEqualToString:currentPort]]
|
|
|
|
};
|
|
|
|
[data addObject:deviceData];
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need to manually add the speaker because it will never show up in the
|
|
|
|
// previous list, as it's not an input.
|
|
|
|
[data addObject:
|
|
|
|
@{ @"type": kDeviceTypeSpeaker,
|
|
|
|
@"name": @"Speaker",
|
|
|
|
@"uid": kDeviceTypeSpeaker,
|
|
|
|
@"selected": [NSNumber numberWithBool:[kDeviceTypeSpeaker isEqualToString:currentPort]]
|
|
|
|
}];
|
|
|
|
|
|
|
|
[self sendEventWithName:kDevicesChanged body:data];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-01-18 19:30:11 +00:00
|
|
|
@end
|