mirror of https://github.com/oxen-io/session-ios
Don't require recording permissions until call is ringing.
We do this by manually managing the RTCAudioSession. Unfortunately to do this we have to include a couple of RTC headers not exported by the default build of WebRTC.framework (see: Libraries/WebRTC) // FREEBIEpull/1/head
parent
ca218ebb64
commit
dbb29d7d7e
@ -0,0 +1,7 @@
|
||||
The RTCAudioSession.h header isn't included in the standard build of
|
||||
WebRTC, so we've vendored it here. Otherwise we're using the vanilla
|
||||
framework.
|
||||
|
||||
We use the RTCAudioSession header to manually manage the RTC audio
|
||||
session, so as to not start recording until the call is connected.
|
||||
|
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "WebRTC/RTCMacros.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString * const kRTCAudioSessionErrorDomain;
|
||||
/** Method that requires lock was called without lock. */
|
||||
extern NSInteger const kRTCAudioSessionErrorLockRequired;
|
||||
/** Unknown configuration error occurred. */
|
||||
extern NSInteger const kRTCAudioSessionErrorConfiguration;
|
||||
|
||||
@class RTCAudioSession;
|
||||
@class RTCAudioSessionConfiguration;
|
||||
|
||||
// Surfaces AVAudioSession events. WebRTC will listen directly for notifications
|
||||
// from AVAudioSession and handle them before calling these delegate methods,
|
||||
// at which point applications can perform additional processing if required.
|
||||
RTC_EXPORT
|
||||
@protocol RTCAudioSessionDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
/** Called on a system notification thread when AVAudioSession starts an
|
||||
* interruption event.
|
||||
*/
|
||||
- (void)audioSessionDidBeginInterruption:(RTCAudioSession *)session;
|
||||
|
||||
/** Called on a system notification thread when AVAudioSession ends an
|
||||
* interruption event.
|
||||
*/
|
||||
- (void)audioSessionDidEndInterruption:(RTCAudioSession *)session
|
||||
shouldResumeSession:(BOOL)shouldResumeSession;
|
||||
|
||||
/** Called on a system notification thread when AVAudioSession changes the
|
||||
* route.
|
||||
*/
|
||||
- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
|
||||
reason:(AVAudioSessionRouteChangeReason)reason
|
||||
previousRoute:(AVAudioSessionRouteDescription *)previousRoute;
|
||||
|
||||
/** Called on a system notification thread when AVAudioSession media server
|
||||
* terminates.
|
||||
*/
|
||||
- (void)audioSessionMediaServicesWereLost:(RTCAudioSession *)session;
|
||||
|
||||
/** Called on a system notification thread when AVAudioSession media server
|
||||
* restarts.
|
||||
*/
|
||||
- (void)audioSessionMediaServicesWereReset:(RTCAudioSession *)session;
|
||||
|
||||
// TODO(tkchin): Maybe handle SilenceSecondaryAudioHintNotification.
|
||||
|
||||
- (void)audioSession:(RTCAudioSession *)session
|
||||
didChangeCanPlayOrRecord:(BOOL)canPlayOrRecord;
|
||||
|
||||
/** Called on a WebRTC thread when the audio device is notified to begin
|
||||
* playback or recording.
|
||||
*/
|
||||
- (void)audioSessionDidStartPlayOrRecord:(RTCAudioSession *)session;
|
||||
|
||||
/** Called on a WebRTC thread when the audio device is notified to stop
|
||||
* playback or recording.
|
||||
*/
|
||||
- (void)audioSessionDidStopPlayOrRecord:(RTCAudioSession *)session;
|
||||
|
||||
@end
|
||||
|
||||
/** Proxy class for AVAudioSession that adds a locking mechanism similar to
|
||||
* AVCaptureDevice. This is used to that interleaving configurations between
|
||||
* WebRTC and the application layer are avoided.
|
||||
*
|
||||
* RTCAudioSession also coordinates activation so that the audio session is
|
||||
* activated only once. See |setActive:error:|.
|
||||
*/
|
||||
RTC_EXPORT
|
||||
@interface RTCAudioSession : NSObject
|
||||
|
||||
/** Convenience property to access the AVAudioSession singleton. Callers should
|
||||
* not call setters on AVAudioSession directly, but other method invocations
|
||||
* are fine.
|
||||
*/
|
||||
@property(nonatomic, readonly) AVAudioSession *session;
|
||||
|
||||
/** Our best guess at whether the session is active based on results of calls to
|
||||
* AVAudioSession.
|
||||
*/
|
||||
@property(nonatomic, readonly) BOOL isActive;
|
||||
/** Whether RTCAudioSession is currently locked for configuration. */
|
||||
@property(nonatomic, readonly) BOOL isLocked;
|
||||
|
||||
/** If YES, WebRTC will not initialize the audio unit automatically when an
|
||||
* audio track is ready for playout or recording. Instead, applications should
|
||||
* call setIsAudioEnabled. If NO, WebRTC will initialize the audio unit
|
||||
* as soon as an audio track is ready for playout or recording.
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL useManualAudio;
|
||||
|
||||
/** This property is only effective if useManualAudio is YES.
|
||||
* Represents permission for WebRTC to initialize the VoIP audio unit.
|
||||
* When set to NO, if the VoIP audio unit used by WebRTC is active, it will be
|
||||
* stopped and uninitialized. This will stop incoming and outgoing audio.
|
||||
* When set to YES, WebRTC will initialize and start the audio unit when it is
|
||||
* needed (e.g. due to establishing an audio connection).
|
||||
* This property was introduced to work around an issue where if an AVPlayer is
|
||||
* playing audio while the VoIP audio unit is initialized, its audio would be
|
||||
* either cut off completely or played at a reduced volume. By preventing
|
||||
* the audio unit from being initialized until after the audio has completed,
|
||||
* we are able to prevent the abrupt cutoff.
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL isAudioEnabled;
|
||||
|
||||
// Proxy properties.
|
||||
@property(readonly) NSString *category;
|
||||
@property(readonly) AVAudioSessionCategoryOptions categoryOptions;
|
||||
@property(readonly) NSString *mode;
|
||||
@property(readonly) BOOL secondaryAudioShouldBeSilencedHint;
|
||||
@property(readonly) AVAudioSessionRouteDescription *currentRoute;
|
||||
@property(readonly) NSInteger maximumInputNumberOfChannels;
|
||||
@property(readonly) NSInteger maximumOutputNumberOfChannels;
|
||||
@property(readonly) float inputGain;
|
||||
@property(readonly) BOOL inputGainSettable;
|
||||
@property(readonly) BOOL inputAvailable;
|
||||
@property(readonly, nullable)
|
||||
NSArray<AVAudioSessionDataSourceDescription *> * inputDataSources;
|
||||
@property(readonly, nullable)
|
||||
AVAudioSessionDataSourceDescription *inputDataSource;
|
||||
@property(readonly, nullable)
|
||||
NSArray<AVAudioSessionDataSourceDescription *> * outputDataSources;
|
||||
@property(readonly, nullable)
|
||||
AVAudioSessionDataSourceDescription *outputDataSource;
|
||||
@property(readonly) double sampleRate;
|
||||
@property(readonly) double preferredSampleRate;
|
||||
@property(readonly) NSInteger inputNumberOfChannels;
|
||||
@property(readonly) NSInteger outputNumberOfChannels;
|
||||
@property(readonly) float outputVolume;
|
||||
@property(readonly) NSTimeInterval inputLatency;
|
||||
@property(readonly) NSTimeInterval outputLatency;
|
||||
@property(readonly) NSTimeInterval IOBufferDuration;
|
||||
@property(readonly) NSTimeInterval preferredIOBufferDuration;
|
||||
|
||||
/** Default constructor. */
|
||||
+ (instancetype)sharedInstance;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/** Adds a delegate, which is held weakly. */
|
||||
- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate;
|
||||
/** Removes an added delegate. */
|
||||
- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate;
|
||||
|
||||
/** Request exclusive access to the audio session for configuration. This call
|
||||
* will block if the lock is held by another object.
|
||||
*/
|
||||
- (void)lockForConfiguration;
|
||||
/** Relinquishes exclusive access to the audio session. */
|
||||
- (void)unlockForConfiguration;
|
||||
|
||||
/** If |active|, activates the audio session if it isn't already active.
|
||||
* Successful calls must be balanced with a setActive:NO when activation is no
|
||||
* longer required. If not |active|, deactivates the audio session if one is
|
||||
* active and this is the last balanced call. When deactivating, the
|
||||
* AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation option is passed to
|
||||
* AVAudioSession.
|
||||
*/
|
||||
- (BOOL)setActive:(BOOL)active
|
||||
error:(NSError **)outError;
|
||||
|
||||
// The following methods are proxies for the associated methods on
|
||||
// AVAudioSession. |lockForConfiguration| must be called before using them
|
||||
// otherwise they will fail with kRTCAudioSessionErrorLockRequired.
|
||||
|
||||
- (BOOL)setCategory:(NSString *)category
|
||||
withOptions:(AVAudioSessionCategoryOptions)options
|
||||
error:(NSError **)outError;
|
||||
- (BOOL)setMode:(NSString *)mode error:(NSError **)outError;
|
||||
- (BOOL)setInputGain:(float)gain error:(NSError **)outError;
|
||||
- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError;
|
||||
- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
|
||||
error:(NSError **)outError;
|
||||
- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
|
||||
error:(NSError **)outError;
|
||||
- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
|
||||
error:(NSError **)outError;
|
||||
- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
|
||||
error:(NSError **)outError;
|
||||
- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
|
||||
error:(NSError **)outError;
|
||||
- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
|
||||
error:(NSError **)outError;
|
||||
- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
|
||||
error:(NSError **)outError;
|
||||
|
||||
@end
|
||||
|
||||
@interface RTCAudioSession (Configuration)
|
||||
|
||||
/** Applies the configuration to the current session. Attempts to set all
|
||||
* properties even if previous ones fail. Only the last error will be
|
||||
* returned.
|
||||
* |lockForConfiguration| must be called first.
|
||||
*/
|
||||
- (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration
|
||||
error:(NSError **)outError;
|
||||
|
||||
/** Convenience method that calls both setConfiguration and setActive.
|
||||
* |lockForConfiguration| must be called first.
|
||||
*/
|
||||
- (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration
|
||||
active:(BOOL)active
|
||||
error:(NSError **)outError;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,51 @@
|
||||
// Copyright © 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebRTC
|
||||
|
||||
/**
|
||||
* By default WebRTC starts the audio session (PlayAndRecord) immediately upon creating the peer connection
|
||||
* but we want to create the peer connection and set up all the signaling channels before we prompt the user
|
||||
* for an incoming call. Without manually handling the session, this would result in the user seeing a recording
|
||||
* permission requested (and recording banner) before they even know they have an incoming call.
|
||||
*
|
||||
* By using the `useManualAudio` and `isAudioEnabled` attributes of the RTCAudioSession we can delay recording until
|
||||
* it makes sense. However, the headers for RTCAudioSession are not exported by default, so we've vendored the header
|
||||
* into our project. See "Libraries/WebRTC"
|
||||
*/
|
||||
class CallAudioSession {
|
||||
|
||||
let TAG = "[CallAudioSession]"
|
||||
/**
|
||||
* The private class that manages AVAudioSession for WebRTC
|
||||
*/
|
||||
private let rtcAudioSession = RTCAudioSession.sharedInstance()
|
||||
|
||||
|
||||
/**
|
||||
* This must be called before any audio tracks are added to the peerConnection, else we'll start recording before all
|
||||
* our signaling is set up.
|
||||
*/
|
||||
func configure() {
|
||||
Logger.info("\(TAG) in \(#function)")
|
||||
rtcAudioSession.useManualAudio = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Because we useManualAudio with our RTCAudioSession, we have to start the recording audio session ourselves.
|
||||
*/
|
||||
func start() {
|
||||
Logger.info("\(TAG) in \(#function)")
|
||||
rtcAudioSession.isAudioEnabled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Because we useManualAudio with our RTCAudioSession, we have to stop the recording audio session ourselves.
|
||||
* Else, we start recording before the next call is ringing.
|
||||
*/
|
||||
func stop() {
|
||||
Logger.info("\(TAG) in \(#function)")
|
||||
rtcAudioSession.isAudioEnabled = false
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
// Copyright © 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import AVKit
|
||||
import WebRTC
|
||||
|
||||
/**
|
||||
* These tests are obtuse - they just assert the exact implementation of the methods. Normally I wouldn't include them,
|
||||
* but these methods make use of a header not included in the standard distribution of the WebRTC.framework. We've
|
||||
* included the header in our local project, and test the methods here to make sure that they are still available when
|
||||
* we upgrade the framework.
|
||||
*
|
||||
* If they are failing, it's possible the RTCAudioSession header, and our usage of it, need to be updated.
|
||||
*/
|
||||
class CallAudioSessionTest: XCTestCase {
|
||||
func testAudioSession() {
|
||||
|
||||
let rtcAudioSession = RTCAudioSession.sharedInstance()
|
||||
// Sanity Check
|
||||
XCTAssertFalse(rtcAudioSession.useManualAudio)
|
||||
|
||||
CallAudioSession().configure()
|
||||
XCTAssertTrue(rtcAudioSession.useManualAudio)
|
||||
XCTAssertFalse(rtcAudioSession.isAudioEnabled)
|
||||
|
||||
CallAudioSession().start()
|
||||
XCTAssertTrue(rtcAudioSession.useManualAudio)
|
||||
XCTAssertTrue(rtcAudioSession.isAudioEnabled)
|
||||
|
||||
CallAudioSession().stop()
|
||||
XCTAssertTrue(rtcAudioSession.useManualAudio)
|
||||
XCTAssertFalse(rtcAudioSession.isAudioEnabled)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue