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)

// FREEBIE
pull/1/head
Michael Kirk 8 years ago
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

@ -20,23 +20,23 @@ https://webrtc.org/native-code/ios/
Once you have your build environment set up and the WebRTC source downloaded:
# The specific set of commands that worked for me were somewhat different.
# 1. Install depot tools
cd <somewhere>
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
cd depot_tools
export PATH=<somewhere>/depot_tools:"$PATH"
# 2. Fetch webrtc source
cd <somewhere else>
mkdir webrtc
cd webrtc
fetch --nohooks webrtc_ios
gclient sync
# 3. Build webrtc
# NOTE: build_ios_libs.sh only worked for me from inside "src"
cd src
webrtc/build/ios/build_ios_libs.sh
# NOTE: It's Carthage/Build/iOS, not Carthage/Builds
mv out_ios_libs/WebRTC.framework ../../Signal-iOS/Carthage/Build/iOS/
# 1. Install depot tools
cd <somewhere>
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
cd depot_tools
export PATH=<somewhere>/depot_tools:"$PATH"
# 2. Fetch webrtc source
cd <somewhere else>
mkdir webrtc
cd webrtc
fetch --nohooks webrtc_ios
gclient sync
# 3. Build webrtc
# NOTE: build_ios_libs.sh only worked for me from inside "src"
cd src
webrtc/build/ios/build_ios_libs.sh
# NOTE: It's Carthage/Build/iOS, not Carthage/Builds
mv out_ios_libs/WebRTC.framework ../../Signal-iOS/Carthage/Build/iOS/
## Translations

@ -99,6 +99,9 @@
45E1F3A51DEF20A100852CF1 /* NoSignalContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E1F3A41DEF20A100852CF1 /* NoSignalContactsView.swift */; };
45E2E9201E153B3D00457AA0 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E2E91F1E153B3D00457AA0 /* Strings.swift */; };
45EB32CF1D7465C900735B2E /* OWSLinkedDevicesTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 45EB32CE1D7465C900735B2E /* OWSLinkedDevicesTableViewController.m */; };
45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */; };
45F170AD1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */; };
45F170AF1E2F0393003FC1F2 /* CallAudioSessionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */; };
45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */; };
45F2B1971D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */; };
45F2B1981D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */; };
@ -690,6 +693,9 @@
45E2E91F1E153B3D00457AA0 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Strings.swift; path = UserInterface/Strings.swift; sourceTree = "<group>"; };
45EB32CD1D7465C900735B2E /* OWSLinkedDevicesTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSLinkedDevicesTableViewController.h; sourceTree = "<group>"; };
45EB32CE1D7465C900735B2E /* OWSLinkedDevicesTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSLinkedDevicesTableViewController.m; sourceTree = "<group>"; };
45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSession.swift; sourceTree = "<group>"; };
45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallAudioSessionTest.swift; path = test/call/CallAudioSessionTest.swift; sourceTree = "<group>"; };
45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = "<group>"; };
45F2B1921D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingMessageCollectionViewCell.h; sourceTree = "<group>"; };
45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingMessageCollectionViewCell.m; sourceTree = "<group>"; };
45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSIncomingMessageCollectionViewCell.xib; sourceTree = "<group>"; };
@ -1405,6 +1411,24 @@
name = Jobs;
sourceTree = "<group>";
};
45F170B01E2F0A35003FC1F2 /* Libraries */ = {
isa = PBXGroup;
children = (
45F170B11E2F0A6A003FC1F2 /* WebRTC */,
);
name = Libraries;
path = Signal;
sourceTree = "<group>";
};
45F170B11E2F0A6A003FC1F2 /* WebRTC */ = {
isa = PBXGroup;
children = (
45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */,
);
name = WebRTC;
path = Libraries/WebRTC;
sourceTree = SOURCE_ROOT;
};
45F659741E1BDA4300444429 /* Redphone */ = {
isa = PBXGroup;
children = (
@ -1510,6 +1534,7 @@
45FBC5D01DF8592E00E9B410 /* SignalCall.swift */,
458DE9D51DEE3FD00071BB03 /* PeerConnectionClient.swift */,
4574A5D51DD6704700C6B692 /* CallService.swift */,
45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */,
);
path = call;
sourceTree = "<group>";
@ -2362,6 +2387,7 @@
isa = PBXGroup;
children = (
D221A093169C9E5E00537ABF /* Signal */,
45F170B01E2F0A35003FC1F2 /* Libraries */,
D221A08C169C9E5E00537ABF /* Frameworks */,
D221A08A169C9E5E00537ABF /* Products */,
70B8009E190C529C0042E3F0 /* spandsp.xcodeproj */,
@ -2426,6 +2452,7 @@
B660F66C1C29867F00687D6E /* test */,
D221A094169C9E5E00537ABF /* Supporting Files */,
B66DBF4919D5BBC8006EA940 /* Images.xcassets */,
45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */,
);
path = Signal;
sourceTree = "<group>";
@ -2689,6 +2716,7 @@
D221A0A9169C9E5F00537ABF = {
DevelopmentTeam = U68MSDN6DR;
LastSwiftMigration = 0800;
ProvisioningStyle = Automatic;
TestTargetID = D221A088169C9E5E00537ABF;
};
};
@ -2902,6 +2930,7 @@
);
inputPaths = (
"$(SRCROOT)/Carthage/Build/iOS/PromiseKit.framework",
"$(SRCROOT)/Carthage/Build/iOS/WebRTC.framework",
);
name = "[Carthage] Copy Frameworks";
outputPaths = (
@ -3100,6 +3129,7 @@
76EB05A018170B33006006FC /* IpAddress.m in Sources */,
FCAC965119FF0A6E0046DFC5 /* MessagesViewController.m in Sources */,
453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */,
45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */,
B68EF9BB1C0B1EBD009C3DCD /* FLAnimatedImageView.m in Sources */,
A5E9D4BB1A65FAD800E4481C /* TSVideoAttachmentAdapter.m in Sources */,
E197B61118BBEC1A00F073E5 /* AudioProcessor.m in Sources */,
@ -3241,6 +3271,7 @@
452ECA4E1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */,
B660F70C1C29988E00687D6E /* StretchFactorController.m in Sources */,
B660F70D1C29988E00687D6E /* AnonymousAudioCallbackHandler.m in Sources */,
45F170AD1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */,
B660F70E1C29988E00687D6E /* RemoteIOAudio.m in Sources */,
B660F70F1C29988E00687D6E /* RemoteIOBufferListWrapper.m in Sources */,
456F6E2F1E261D1000FD2210 /* PeerConnectionClientTest.swift in Sources */,
@ -3307,6 +3338,7 @@
B660F7441C29988E00687D6E /* DhPacketSharedSecretHashes.m in Sources */,
B660F7451C29988E00687D6E /* HandshakePacket.m in Sources */,
B660F7461C29988E00687D6E /* HelloAckPacket.m in Sources */,
45F170AF1E2F0393003FC1F2 /* CallAudioSessionTest.swift in Sources */,
456F6E231E24133500FD2210 /* Platform.swift in Sources */,
B660F7471C29988E00687D6E /* HelloPacket.m in Sources */,
B660F7481C29988E00687D6E /* RecipientUnavailable.m in Sources */,
@ -3756,6 +3788,7 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEFINES_MODULE = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -3814,6 +3847,7 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEFINES_MODULE = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -3854,7 +3888,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "org.whispersystems.${PRODUCT_NAME:rfc1034identifier}";
PRODUCT_NAME = SignalTests;
PROVISIONING_PROFILE = "c15eac58-5aa7-4660-b874-b9f7ed3dab70";
PROVISIONING_PROFILE = "";
SWIFT_OBJC_BRIDGING_HEADER = "Signal/test/SignalTests-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;

@ -52,3 +52,4 @@
#import <SignalServiceKit/TSStorageManager+SessionStore.h>
#import <SignalServiceKit/TSStorageManager+keyingMaterial.h>
#import <SignalServiceKit/TSThread.h>
#import <WebRTC/RTCAudioSession.h>

@ -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
}
}

@ -10,14 +10,15 @@ import WebRTC
*
* It serves as connection from the `CallUIAdapater` to the `PeerConnectionClient`.
*
*
* ## Signaling
*
* Signaling refers to the setup and tear down of the connection. Before the connection is established, this must happen
* out of band (using Signal Service), but once the connection is established it's possible to publish updates
* (like hangup) via the established channel.
*
* Following is a high level process of the exchange of messages that must take place for this to happen.
* Signaling state is synchronized on the `signalingQueue` and only mutated in the handleXXX family of methods.
*
* Following is a high level process of the exchange of messages that takes place during call signaling.
*
* ### Key
*
@ -30,20 +31,22 @@ import WebRTC
*
* | Caller | Callee |
* +----------------------------+-------------------------+
* Start outgoing call: `handleOutgoingCall`
* Start outgoing call: `handleOutgoingCall`...
--[SS.CallOffer]-->
* and start generating and storing ICE updates.
* (As ICE candites are generated: `handleLocalAddedIceCandidate`)
* ...and start generating ICE updates.
* As ICE candidates are generated, `handleLocalAddedIceCandidate` is called.
* and we *store* the ICE updates for later.
*
* Received call offer: `handleReceivedOffer`
* Send call answer
* <--[SS.CallAnswer]--
* Start generating ICE updates and send them as
* they are generated: `handleLocalAddedIceCandidate`
* Start generating ICE updates.
* As they are generated `handleLocalAddedIceCandidate` is called
which immediately sends the ICE updates to the Caller.
* <--[SS.ICEUpdate]-- (sent multiple times)
*
* Received CallAnswer: `handleReceivedAnswer`
* so send any stored ice updates
* So send any stored ice updates (and send future ones immediately)
* --[SS.ICEUpdates]-->
*
* Once compatible ICE updates have been exchanged...
@ -755,6 +758,7 @@ fileprivate let timeoutSeconds = 60
return
}
callUIAdapter.recipientAcceptedCall(call)
handleConnectedCall(call)
} else if message.hasHangup() {
@ -885,6 +889,7 @@ fileprivate let timeoutSeconds = 60
assertOnSignalingQueue()
Logger.debug("\(TAG) in \(#function)")
PeerConnectionClient.stopAudioSession()
peerConnectionClient?.delegate = nil
peerConnectionClient?.terminate()

@ -49,6 +49,7 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
func answerCall(_ call: SignalCall) {
CallService.signalingQueue.async {
PeerConnectionClient.startAudioSession()
self.callService.handleAnswerCall(call)
}
}
@ -59,6 +60,10 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
}
}
func recipientAcceptedCall(_ call: SignalCall) {
PeerConnectionClient.startAudioSession()
}
func endCall(_ call: SignalCall) {
CallService.signalingQueue.async {
self.callService.handleLocalHungupCall(call)

@ -74,6 +74,7 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
private var audioSender: RTCRtpSender?
private var audioTrack: RTCAudioTrack?
private var audioConstraints: RTCMediaConstraints
static private let sharedAudioSession = CallAudioSession()
// Video
@ -90,6 +91,7 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
configuration.bundlePolicy = .maxBundle
configuration.rtcpMuxPolicy = .require
let connectionConstraintsDict = ["DtlsSrtpKeyAgreement": "true"]
connectionConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: connectionConstraintsDict)
@ -98,6 +100,8 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
super.init()
// Configure audio session so we don't prompt user with Record permission until call is connected.
type(of: self).configureAudioSession()
peerConnection = factory.peerConnection(with: configuration,
constraints: connectionConstraints,
delegate: self)
@ -395,6 +399,21 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
self.dataChannel = dataChannel
}
}
// Mark: Audio Session
class func configureAudioSession() {
sharedAudioSession.configure()
}
class func startAudioSession() {
sharedAudioSession.start()
}
class func stopAudioSession() {
sharedAudioSession.stop()
}
}
/**

@ -91,6 +91,10 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
callManager.end(call: call)
}
func recipientAcceptedCall(_ call: SignalCall) {
// no - op
}
func endCall(_ call: SignalCall) {
callManager.end(call: call)
}
@ -126,15 +130,6 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
Logger.debug("\(TAG) in \(#function) CXStartCallAction")
/*
Configure the audio session, but do not start call audio here, since it must be done once
the audio session has been activated by the system after having its priority elevated.
*/
// TODO
// copied from Speakerbox, but is there a corallary with peerconnection, since peer connection starts the audio
// session when adding an audiotrack
//configureAudioSession()
// TODO does this work when `action.handle.value` is not in e164 format, e.g. if called via intent?
guard let call = callManager.callWithLocalId(action.callUUID) else {
Logger.error("\(TAG) unable to find call in \(#function)")
@ -228,9 +223,11 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
//
// // Stop or start audio in response to holding or unholding the call.
// if call.isOnHold {
// stopAudio()
// // stopAudio() <-- SpeakerBox
// PeerConnectionClient.stopAudioSession()
// } else {
// startAudio()
// // startAudio() <-- SpeakerBox
// PeerConnectionClient.startAudioSession()
// }
// Signal to the system that the action has been successfully performed.
@ -268,10 +265,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
Logger.debug("\(TAG) Received \(#function)")
// TODO
// copied from Speakerbox, but is there a corallary with peerconnection, since peer connection starts the audio
// session when adding an audiotrack
// startAudio()
// Start recording
PeerConnectionClient.startAudioSession()
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {

@ -13,6 +13,7 @@ protocol CallUIAdaptee {
func reportMissedCall(_ call: SignalCall, callerName: String)
func answerCall(_ call: SignalCall)
func declineCall(_ call: SignalCall)
func recipientAcceptedCall(_ call: SignalCall)
func endCall(_ call: SignalCall)
func toggleMute(call: SignalCall, isMuted: Bool)
}
@ -80,6 +81,10 @@ class CallUIAdapter {
adaptee.declineCall(call)
}
internal func recipientAcceptedCall(_ call: SignalCall) {
adaptee.recipientAcceptedCall(call)
}
internal func endCall(_ call: SignalCall) {
adaptee.endCall(call)
}

@ -61,27 +61,12 @@ import PromiseKit
Logger.debug("\(TAG) \(#function)")
audioManager.setAudioEnabled(true)
audioManager.handleInboundRing()
do {
// Respect silent switch.
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient)
Logger.debug("\(TAG) set audio category to SoloAmbient")
} catch {
Logger.error("\(TAG) failed to change audio category to soloAmbient in \(#function)")
}
vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(vibrate), userInfo: nil, repeats: true)
}
private func handleConnected() {
Logger.debug("\(TAG) \(#function)")
stopRinging()
do {
// Start recording
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord)
Logger.debug("\(TAG) set audio category to PlayAndRecord")
} catch {
Logger.error("\(TAG) failed to change audio category to soloAmbient in \(#function)")
}
}
private func handleLocalFailure() {

@ -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…
Cancel
Save