1/* 2 * Copyright 2016 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11#import <Foundation/Foundation.h> 12#import <OCMock/OCMock.h> 13#import <XCTest/XCTest.h> 14 15#include <vector> 16 17#include "rtc_base/event.h" 18#include "rtc_base/gunit.h" 19 20#import "components/audio/RTCAudioSession+Private.h" 21 22#import "components/audio/RTCAudioSession.h" 23#import "components/audio/RTCAudioSessionConfiguration.h" 24 25@interface RTC_OBJC_TYPE (RTCAudioSession) 26(UnitTesting) 27 28 @property(nonatomic, 29 readonly) std::vector<__weak id<RTC_OBJC_TYPE(RTCAudioSessionDelegate)> > delegates; 30 31- (instancetype)initWithAudioSession:(id)audioSession; 32 33@end 34 35@interface MockAVAudioSession : NSObject 36 37@property (nonatomic, readwrite, assign) float outputVolume; 38 39@end 40 41@implementation MockAVAudioSession 42@synthesize outputVolume = _outputVolume; 43@end 44 45@interface RTCAudioSessionTestDelegate : NSObject <RTC_OBJC_TYPE (RTCAudioSessionDelegate)> 46 47@property (nonatomic, readonly) float outputVolume; 48 49@end 50 51@implementation RTCAudioSessionTestDelegate 52 53@synthesize outputVolume = _outputVolume; 54 55- (instancetype)init { 56 if (self = [super init]) { 57 _outputVolume = -1; 58 } 59 return self; 60} 61 62- (void)audioSessionDidBeginInterruption:(RTC_OBJC_TYPE(RTCAudioSession) *)session { 63} 64 65- (void)audioSessionDidEndInterruption:(RTC_OBJC_TYPE(RTCAudioSession) *)session 66 shouldResumeSession:(BOOL)shouldResumeSession { 67} 68 69- (void)audioSessionDidChangeRoute:(RTC_OBJC_TYPE(RTCAudioSession) *)session 70 reason:(AVAudioSessionRouteChangeReason)reason 71 previousRoute:(AVAudioSessionRouteDescription *)previousRoute { 72} 73 74- (void)audioSessionMediaServerTerminated:(RTC_OBJC_TYPE(RTCAudioSession) *)session { 75} 76 77- (void)audioSessionMediaServerReset:(RTC_OBJC_TYPE(RTCAudioSession) *)session { 78} 79 80- (void)audioSessionShouldConfigure:(RTC_OBJC_TYPE(RTCAudioSession) *)session { 81} 82 83- (void)audioSessionShouldUnconfigure:(RTC_OBJC_TYPE(RTCAudioSession) *)session { 84} 85 86- (void)audioSession:(RTC_OBJC_TYPE(RTCAudioSession) *)audioSession 87 didChangeOutputVolume:(float)outputVolume { 88 _outputVolume = outputVolume; 89} 90 91@end 92 93// A delegate that adds itself to the audio session on init and removes itself 94// in its dealloc. 95@interface RTCTestRemoveOnDeallocDelegate : RTCAudioSessionTestDelegate 96@end 97 98@implementation RTCTestRemoveOnDeallocDelegate 99 100- (instancetype)init { 101 if (self = [super init]) { 102 RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; 103 [session addDelegate:self]; 104 } 105 return self; 106} 107 108- (void)dealloc { 109 RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; 110 [session removeDelegate:self]; 111} 112 113@end 114 115@interface RTCAudioSessionTest : XCTestCase 116 117@end 118 119@implementation RTCAudioSessionTest 120 121- (void)testAddAndRemoveDelegates { 122 RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; 123 NSMutableArray *delegates = [NSMutableArray array]; 124 const size_t count = 5; 125 for (size_t i = 0; i < count; ++i) { 126 RTCAudioSessionTestDelegate *delegate = 127 [[RTCAudioSessionTestDelegate alloc] init]; 128 [session addDelegate:delegate]; 129 [delegates addObject:delegate]; 130 EXPECT_EQ(i + 1, session.delegates.size()); 131 } 132 [delegates enumerateObjectsUsingBlock:^(RTCAudioSessionTestDelegate *obj, 133 NSUInteger idx, 134 BOOL *stop) { 135 [session removeDelegate:obj]; 136 }]; 137 EXPECT_EQ(0u, session.delegates.size()); 138} 139 140- (void)testPushDelegate { 141 RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; 142 NSMutableArray *delegates = [NSMutableArray array]; 143 const size_t count = 2; 144 for (size_t i = 0; i < count; ++i) { 145 RTCAudioSessionTestDelegate *delegate = 146 [[RTCAudioSessionTestDelegate alloc] init]; 147 [session addDelegate:delegate]; 148 [delegates addObject:delegate]; 149 } 150 // Test that it gets added to the front of the list. 151 RTCAudioSessionTestDelegate *pushedDelegate = 152 [[RTCAudioSessionTestDelegate alloc] init]; 153 [session pushDelegate:pushedDelegate]; 154 EXPECT_TRUE(pushedDelegate == session.delegates[0]); 155 156 // Test that it stays at the front of the list. 157 for (size_t i = 0; i < count; ++i) { 158 RTCAudioSessionTestDelegate *delegate = 159 [[RTCAudioSessionTestDelegate alloc] init]; 160 [session addDelegate:delegate]; 161 [delegates addObject:delegate]; 162 } 163 EXPECT_TRUE(pushedDelegate == session.delegates[0]); 164 165 // Test that the next one goes to the front too. 166 pushedDelegate = [[RTCAudioSessionTestDelegate alloc] init]; 167 [session pushDelegate:pushedDelegate]; 168 EXPECT_TRUE(pushedDelegate == session.delegates[0]); 169} 170 171// Tests that delegates added to the audio session properly zero out. This is 172// checking an implementation detail (that vectors of __weak work as expected). 173- (void)testZeroingWeakDelegate { 174 RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; 175 @autoreleasepool { 176 // Add a delegate to the session. There should be one delegate at this 177 // point. 178 RTCAudioSessionTestDelegate *delegate = 179 [[RTCAudioSessionTestDelegate alloc] init]; 180 [session addDelegate:delegate]; 181 EXPECT_EQ(1u, session.delegates.size()); 182 EXPECT_TRUE(session.delegates[0]); 183 } 184 // The previously created delegate should've de-alloced, leaving a nil ptr. 185 EXPECT_FALSE(session.delegates[0]); 186 RTCAudioSessionTestDelegate *delegate = 187 [[RTCAudioSessionTestDelegate alloc] init]; 188 [session addDelegate:delegate]; 189 // On adding a new delegate, nil ptrs should've been cleared. 190 EXPECT_EQ(1u, session.delegates.size()); 191 EXPECT_TRUE(session.delegates[0]); 192} 193 194// Tests that we don't crash when removing delegates in dealloc. 195// Added as a regression test. 196- (void)testRemoveDelegateOnDealloc { 197 @autoreleasepool { 198 RTCTestRemoveOnDeallocDelegate *delegate = 199 [[RTCTestRemoveOnDeallocDelegate alloc] init]; 200 EXPECT_TRUE(delegate); 201 } 202 RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; 203 EXPECT_EQ(0u, session.delegates.size()); 204} 205 206- (void)testAudioSessionActivation { 207 RTC_OBJC_TYPE(RTCAudioSession) *audioSession = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]; 208 EXPECT_EQ(0, audioSession.activationCount); 209 [audioSession audioSessionDidActivate:[AVAudioSession sharedInstance]]; 210 EXPECT_EQ(1, audioSession.activationCount); 211 [audioSession audioSessionDidDeactivate:[AVAudioSession sharedInstance]]; 212 EXPECT_EQ(0, audioSession.activationCount); 213} 214 215// Hack - fixes OCMVerify link error 216// Link error is: Undefined symbols for architecture i386: 217// "OCMMakeLocation(objc_object*, char const*, int)", referenced from: 218// -[RTCAudioSessionTest testConfigureWebRTCSession] in RTCAudioSessionTest.o 219// ld: symbol(s) not found for architecture i386 220// REASON: https://github.com/erikdoe/ocmock/issues/238 221OCMLocation *OCMMakeLocation(id testCase, const char *fileCString, int line){ 222 return [OCMLocation locationWithTestCase:testCase 223 file:[NSString stringWithUTF8String:fileCString] 224 line:line]; 225} 226 227- (void)testConfigureWebRTCSession { 228 NSError *error = nil; 229 230 void (^setActiveBlock)(NSInvocation *invocation) = ^(NSInvocation *invocation) { 231 __autoreleasing NSError **retError; 232 [invocation getArgument:&retError atIndex:4]; 233 *retError = [NSError errorWithDomain:@"AVAudioSession" 234 code:AVAudioSessionErrorCodeCannotInterruptOthers 235 userInfo:nil]; 236 BOOL failure = NO; 237 [invocation setReturnValue:&failure]; 238 }; 239 240 id mockAVAudioSession = OCMPartialMock([AVAudioSession sharedInstance]); 241 OCMStub([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES 242 withOptions:0 243 error:([OCMArg anyObjectRef])]) 244 .andDo(setActiveBlock); 245 246 id mockAudioSession = OCMPartialMock([RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]); 247 OCMStub([mockAudioSession session]).andReturn(mockAVAudioSession); 248 249 RTC_OBJC_TYPE(RTCAudioSession) *audioSession = mockAudioSession; 250 EXPECT_EQ(0, audioSession.activationCount); 251 [audioSession lockForConfiguration]; 252 // configureWebRTCSession is forced to fail in the above mock interface, 253 // so activationCount should remain 0 254 OCMExpect([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES 255 withOptions:0 256 error:([OCMArg anyObjectRef])]) 257 .andDo(setActiveBlock); 258 OCMExpect([mockAudioSession session]).andReturn(mockAVAudioSession); 259 EXPECT_FALSE([audioSession configureWebRTCSession:&error]); 260 EXPECT_EQ(0, audioSession.activationCount); 261 262 id session = audioSession.session; 263 EXPECT_EQ(session, mockAVAudioSession); 264 EXPECT_EQ(NO, [mockAVAudioSession setActive:YES withOptions:0 error:&error]); 265 [audioSession unlockForConfiguration]; 266 267 OCMVerify([mockAudioSession session]); 268 OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES withOptions:0 error:&error]); 269 OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:NO withOptions:0 error:&error]); 270 271 [mockAVAudioSession stopMocking]; 272 [mockAudioSession stopMocking]; 273} 274 275- (void)testConfigureWebRTCSessionWithoutLocking { 276 NSError *error = nil; 277 278 id mockAVAudioSession = OCMPartialMock([AVAudioSession sharedInstance]); 279 id mockAudioSession = OCMPartialMock([RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]); 280 OCMStub([mockAudioSession session]).andReturn(mockAVAudioSession); 281 282 RTC_OBJC_TYPE(RTCAudioSession) *audioSession = mockAudioSession; 283 284 std::unique_ptr<rtc::Thread> thread = rtc::Thread::Create(); 285 EXPECT_TRUE(thread); 286 EXPECT_TRUE(thread->Start()); 287 288 rtc::Event waitLock; 289 rtc::Event waitCleanup; 290 constexpr webrtc::TimeDelta timeout = webrtc::TimeDelta::Seconds(5); 291 thread->PostTask([audioSession, &waitLock, &waitCleanup, timeout] { 292 [audioSession lockForConfiguration]; 293 waitLock.Set(); 294 waitCleanup.Wait(timeout); 295 [audioSession unlockForConfiguration]; 296 }); 297 298 waitLock.Wait(timeout); 299 [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:0 error:&error]; 300 EXPECT_TRUE(error != nil); 301 EXPECT_EQ(error.domain, kRTCAudioSessionErrorDomain); 302 EXPECT_EQ(error.code, kRTCAudioSessionErrorLockRequired); 303 waitCleanup.Set(); 304 thread->Stop(); 305 306 [mockAVAudioSession stopMocking]; 307 [mockAudioSession stopMocking]; 308} 309 310- (void)testAudioVolumeDidNotify { 311 MockAVAudioSession *mockAVAudioSession = [[MockAVAudioSession alloc] init]; 312 RTC_OBJC_TYPE(RTCAudioSession) *session = 313 [[RTC_OBJC_TYPE(RTCAudioSession) alloc] initWithAudioSession:mockAVAudioSession]; 314 RTCAudioSessionTestDelegate *delegate = 315 [[RTCAudioSessionTestDelegate alloc] init]; 316 [session addDelegate:delegate]; 317 318 float expectedVolume = 0.75; 319 mockAVAudioSession.outputVolume = expectedVolume; 320 321 EXPECT_EQ(expectedVolume, delegate.outputVolume); 322} 323 324@end 325