您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

AudioMode.m 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. /*
  2. * Copyright @ 2017-present 8x8, Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <AVFoundation/AVFoundation.h>
  17. #import <React/RCTEventEmitter.h>
  18. #import <React/RCTLog.h>
  19. #import <WebRTC/WebRTC.h>
  20. #import "JitsiAudioSession+Private.h"
  21. #import "LogUtils.h"
  22. // Audio mode
  23. typedef enum {
  24. kAudioModeDefault,
  25. kAudioModeAudioCall,
  26. kAudioModeVideoCall
  27. } JitsiMeetAudioMode;
  28. // Events
  29. static NSString * const kDevicesChanged = @"org.jitsi.meet:features/audio-mode#devices-update";
  30. // Device types (must match JS and Java)
  31. static NSString * const kDeviceTypeHeadphones = @"HEADPHONES";
  32. static NSString * const kDeviceTypeBluetooth = @"BLUETOOTH";
  33. static NSString * const kDeviceTypeEarpiece = @"EARPIECE";
  34. static NSString * const kDeviceTypeSpeaker = @"SPEAKER";
  35. static NSString * const kDeviceTypeUnknown = @"UNKNOWN";
  36. @interface AudioMode : RCTEventEmitter<RTCAudioSessionDelegate>
  37. @property(nonatomic, strong) dispatch_queue_t workerQueue;
  38. @end
  39. @implementation AudioMode {
  40. JitsiMeetAudioMode activeMode;
  41. RTCAudioSessionConfiguration *defaultConfig;
  42. RTCAudioSessionConfiguration *audioCallConfig;
  43. RTCAudioSessionConfiguration *videoCallConfig;
  44. RTCAudioSessionConfiguration *earpieceConfig;
  45. BOOL forceSpeaker;
  46. BOOL forceEarpiece;
  47. BOOL isSpeakerOn;
  48. BOOL isEarpieceOn;
  49. }
  50. RCT_EXPORT_MODULE();
  51. + (BOOL)requiresMainQueueSetup {
  52. return NO;
  53. }
  54. - (NSArray<NSString *> *)supportedEvents {
  55. return @[ kDevicesChanged ];
  56. }
  57. - (NSDictionary *)constantsToExport {
  58. return @{
  59. @"DEVICE_CHANGE_EVENT": kDevicesChanged,
  60. @"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall],
  61. @"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault],
  62. @"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall]
  63. };
  64. };
  65. - (instancetype)init {
  66. self = [super init];
  67. if (self) {
  68. dispatch_queue_attr_t attributes =
  69. dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
  70. _workerQueue = dispatch_queue_create("AudioMode.queue", attributes);
  71. activeMode = kAudioModeDefault;
  72. defaultConfig = [[RTCAudioSessionConfiguration alloc] init];
  73. defaultConfig.category = AVAudioSessionCategoryAmbient;
  74. defaultConfig.categoryOptions = 0;
  75. defaultConfig.mode = AVAudioSessionModeDefault;
  76. audioCallConfig = [[RTCAudioSessionConfiguration alloc] init];
  77. audioCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
  78. audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker;
  79. audioCallConfig.mode = AVAudioSessionModeVoiceChat;
  80. videoCallConfig = [[RTCAudioSessionConfiguration alloc] init];
  81. videoCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
  82. videoCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
  83. videoCallConfig.mode = AVAudioSessionModeVideoChat;
  84. // Manually routing audio to the earpiece doesn't quite work unless one disables BT (weird, I know).
  85. earpieceConfig = [[RTCAudioSessionConfiguration alloc] init];
  86. earpieceConfig.category = AVAudioSessionCategoryPlayAndRecord;
  87. earpieceConfig.categoryOptions = 0;
  88. earpieceConfig.mode = AVAudioSessionModeVoiceChat;
  89. forceSpeaker = NO;
  90. forceEarpiece = NO;
  91. isSpeakerOn = NO;
  92. isEarpieceOn = NO;
  93. RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  94. [session addDelegate:self];
  95. }
  96. return self;
  97. }
  98. - (dispatch_queue_t)methodQueue {
  99. // Use a dedicated queue for audio mode operations.
  100. return _workerQueue;
  101. }
  102. - (BOOL)setConfigWithoutLock:(RTCAudioSessionConfiguration *)config
  103. error:(NSError * _Nullable *)outError {
  104. RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  105. return [session setConfiguration:config error:outError];
  106. }
  107. - (BOOL)setConfig:(RTCAudioSessionConfiguration *)config
  108. error:(NSError * _Nullable *)outError {
  109. RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  110. [session lockForConfiguration];
  111. BOOL success = [self setConfigWithoutLock:config error:outError];
  112. [session unlockForConfiguration];
  113. return success;
  114. }
  115. #pragma mark - Exported methods
  116. RCT_EXPORT_METHOD(setMode:(int)mode
  117. resolve:(RCTPromiseResolveBlock)resolve
  118. reject:(RCTPromiseRejectBlock)reject) {
  119. RTCAudioSessionConfiguration *config = [self configForMode:mode];
  120. NSError *error;
  121. if (config == nil) {
  122. reject(@"setMode", @"Invalid mode", nil);
  123. return;
  124. }
  125. // Reset.
  126. if (mode == kAudioModeDefault) {
  127. forceSpeaker = NO;
  128. forceEarpiece = NO;
  129. }
  130. activeMode = mode;
  131. if ([self setConfig:config error:&error]) {
  132. resolve(nil);
  133. } else {
  134. reject(@"setMode", error.localizedDescription, error);
  135. }
  136. [self notifyDevicesChanged];
  137. }
  138. RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
  139. resolve:(RCTPromiseResolveBlock)resolve
  140. reject:(RCTPromiseRejectBlock)reject) {
  141. DDLogInfo(@"[AudioMode] Selected device: %@", device);
  142. RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  143. [session lockForConfiguration];
  144. BOOL success;
  145. NSError *error = nil;
  146. // Reset these, as we are about to compute them.
  147. forceSpeaker = NO;
  148. forceEarpiece = NO;
  149. // The speaker is special, so test for it first.
  150. if ([device isEqualToString:kDeviceTypeSpeaker]) {
  151. forceSpeaker = NO;
  152. success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
  153. } else {
  154. // Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
  155. AVAudioSession *_session = [AVAudioSession sharedInstance];
  156. AVAudioSessionPortDescription *port = nil;
  157. // Find the matching input device.
  158. for (AVAudioSessionPortDescription *portDesc in _session.availableInputs) {
  159. if ([portDesc.UID isEqualToString:device]) {
  160. port = portDesc;
  161. break;
  162. }
  163. }
  164. if (port != nil) {
  165. // First remove the override if we are going to select a different device.
  166. if (isSpeakerOn) {
  167. [session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
  168. }
  169. // Special case for the earpiece.
  170. if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
  171. forceEarpiece = YES;
  172. [self setConfigWithoutLock:earpieceConfig error:nil];
  173. } else if (isEarpieceOn) {
  174. // Reset the config.
  175. RTCAudioSessionConfiguration *config = [self configForMode:activeMode];
  176. [self setConfigWithoutLock:config error:nil];
  177. }
  178. // Select our preferred input.
  179. success = [session setPreferredInput:port error:&error];
  180. } else {
  181. success = NO;
  182. error = RCTErrorWithMessage(@"Could not find audio device");
  183. }
  184. }
  185. [session unlockForConfiguration];
  186. if (success) {
  187. resolve(nil);
  188. } else {
  189. reject(@"setAudioDevice", error != nil ? error.localizedDescription : @"", error);
  190. }
  191. }
  192. RCT_EXPORT_METHOD(updateDeviceList) {
  193. [self notifyDevicesChanged];
  194. }
  195. #pragma mark - RTCAudioSessionDelegate
  196. - (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
  197. reason:(AVAudioSessionRouteChangeReason)reason
  198. previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
  199. // Update JS about the changes.
  200. [self notifyDevicesChanged];
  201. dispatch_async(_workerQueue, ^{
  202. switch (reason) {
  203. case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
  204. case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
  205. // If the device list changed, reset our overrides.
  206. self->forceSpeaker = NO;
  207. self->forceEarpiece = NO;
  208. break;
  209. case AVAudioSessionRouteChangeReasonCategoryChange:
  210. // The category has changed. Check if it's the one we want and adjust as
  211. // needed.
  212. break;
  213. default:
  214. return;
  215. }
  216. // We don't want to touch the category when in default mode.
  217. // This is to play well with other components which could be integrated
  218. // into the final application.
  219. if (self->activeMode != kAudioModeDefault) {
  220. DDLogInfo(@"[AudioMode] Route changed, reapplying RTCAudioSession config");
  221. RTCAudioSessionConfiguration *config = [self configForMode:self->activeMode];
  222. [self setConfig:config error:nil];
  223. if (self->forceSpeaker && !self->isSpeakerOn) {
  224. RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  225. [session lockForConfiguration];
  226. [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
  227. [session unlockForConfiguration];
  228. }
  229. }
  230. });
  231. }
  232. - (void)audioSession:(RTCAudioSession *)audioSession didSetActive:(BOOL)active {
  233. DDLogInfo(@"[AudioMode] Audio session didSetActive:%d", active);
  234. }
  235. #pragma mark - Helper methods
  236. - (RTCAudioSessionConfiguration *)configForMode:(int) mode {
  237. if (mode != kAudioModeDefault && forceEarpiece) {
  238. return earpieceConfig;
  239. }
  240. switch (mode) {
  241. case kAudioModeAudioCall:
  242. return audioCallConfig;
  243. case kAudioModeDefault:
  244. return defaultConfig;
  245. case kAudioModeVideoCall:
  246. return videoCallConfig;
  247. default:
  248. return nil;
  249. }
  250. }
  251. // Here we convert input and output port types into a single type.
  252. - (NSString *)portTypeToString:(AVAudioSessionPort) portType {
  253. if ([portType isEqualToString:AVAudioSessionPortHeadphones]
  254. || [portType isEqualToString:AVAudioSessionPortHeadsetMic]) {
  255. return kDeviceTypeHeadphones;
  256. } else if ([portType isEqualToString:AVAudioSessionPortBuiltInMic]
  257. || [portType isEqualToString:AVAudioSessionPortBuiltInReceiver]) {
  258. return kDeviceTypeEarpiece;
  259. } else if ([portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
  260. return kDeviceTypeSpeaker;
  261. } else if ([portType isEqualToString:AVAudioSessionPortBluetoothHFP]
  262. || [portType isEqualToString:AVAudioSessionPortBluetoothLE]
  263. || [portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) {
  264. return kDeviceTypeBluetooth;
  265. } else {
  266. return kDeviceTypeUnknown;
  267. }
  268. }
  269. - (void)notifyDevicesChanged {
  270. dispatch_async(_workerQueue, ^{
  271. NSMutableArray *data = [[NSMutableArray alloc] init];
  272. // Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
  273. AVAudioSession *session = [AVAudioSession sharedInstance];
  274. NSString *currentPort = @"";
  275. AVAudioSessionRouteDescription *currentRoute = session.currentRoute;
  276. // Check what the current device is. Because the speaker is somewhat special, we need to
  277. // check for it first.
  278. if (currentRoute != nil) {
  279. AVAudioSessionPortDescription *output = currentRoute.outputs.firstObject;
  280. AVAudioSessionPortDescription *input = currentRoute.inputs.firstObject;
  281. if (output != nil && [output.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
  282. currentPort = kDeviceTypeSpeaker;
  283. self->isSpeakerOn = YES;
  284. } else if (input != nil) {
  285. currentPort = input.UID;
  286. self->isSpeakerOn = NO;
  287. self->isEarpieceOn = [input.portType isEqualToString:AVAudioSessionPortBuiltInMic];
  288. }
  289. }
  290. BOOL headphonesAvailable = NO;
  291. for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
  292. if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic] || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
  293. headphonesAvailable = YES;
  294. break;
  295. }
  296. }
  297. for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
  298. // Skip "Phone" if headphones are present.
  299. if (headphonesAvailable && [portDesc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
  300. continue;
  301. }
  302. id deviceData
  303. = @{
  304. @"type": [self portTypeToString:portDesc.portType],
  305. @"name": portDesc.portName,
  306. @"uid": portDesc.UID,
  307. @"selected": [NSNumber numberWithBool:[portDesc.UID isEqualToString:currentPort]]
  308. };
  309. [data addObject:deviceData];
  310. }
  311. // We need to manually add the speaker because it will never show up in the
  312. // previous list, as it's not an input.
  313. [data addObject:
  314. @{ @"type": kDeviceTypeSpeaker,
  315. @"name": @"Speaker",
  316. @"uid": kDeviceTypeSpeaker,
  317. @"selected": [NSNumber numberWithBool:[kDeviceTypeSpeaker isEqualToString:currentPort]]
  318. }];
  319. [self sendEventWithName:kDevicesChanged body:data];
  320. });
  321. }
  322. @end