xref: /aosp_15_r20/external/webrtc/sdk/objc/unittests/RTCAudioSessionTest.mm (revision d9f758449e529ab9291ac668be2861e7a55c2422)
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