diff --git a/Libraries/WebRTC/README.md b/Libraries/WebRTC/README.md new file mode 100644 index 000000000..119fded51 --- /dev/null +++ b/Libraries/WebRTC/README.md @@ -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. + diff --git a/Libraries/WebRTC/RTCAudioSession.h b/Libraries/WebRTC/RTCAudioSession.h new file mode 100644 index 000000000..ef5cec460 --- /dev/null +++ b/Libraries/WebRTC/RTCAudioSession.h @@ -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 +#import + +#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 + +@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 * inputDataSources; +@property(readonly, nullable) + AVAudioSessionDataSourceDescription *inputDataSource; +@property(readonly, nullable) + NSArray * 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)delegate; +/** Removes an added delegate. */ +- (void)removeDelegate:(id)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 diff --git a/MAINTAINING.md b/MAINTAINING.md index 9a98e31c1..68fbe7482 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -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 - git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git - cd depot_tools - export PATH=/depot_tools:"$PATH" - # 2. Fetch webrtc source - cd - 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 + git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git + cd depot_tools + export PATH=/depot_tools:"$PATH" + # 2. Fetch webrtc source + cd + 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 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 9cc6c46ff..323df8580 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 45EB32CD1D7465C900735B2E /* OWSLinkedDevicesTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSLinkedDevicesTableViewController.h; sourceTree = ""; }; 45EB32CE1D7465C900735B2E /* OWSLinkedDevicesTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSLinkedDevicesTableViewController.m; sourceTree = ""; }; + 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSession.swift; sourceTree = ""; }; + 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallAudioSessionTest.swift; path = test/call/CallAudioSessionTest.swift; sourceTree = ""; }; + 45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = ""; }; 45F2B1921D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingMessageCollectionViewCell.h; sourceTree = ""; }; 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingMessageCollectionViewCell.m; sourceTree = ""; }; 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSIncomingMessageCollectionViewCell.xib; sourceTree = ""; }; @@ -1405,6 +1411,24 @@ name = Jobs; sourceTree = ""; }; + 45F170B01E2F0A35003FC1F2 /* Libraries */ = { + isa = PBXGroup; + children = ( + 45F170B11E2F0A6A003FC1F2 /* WebRTC */, + ); + name = Libraries; + path = Signal; + sourceTree = ""; + }; + 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 = ""; @@ -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 = ""; @@ -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; diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 044be65f3..aadc349fd 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -52,3 +52,4 @@ #import #import #import +#import diff --git a/Signal/src/call/CallAudioSession.swift b/Signal/src/call/CallAudioSession.swift new file mode 100644 index 000000000..f1925c2d0 --- /dev/null +++ b/Signal/src/call/CallAudioSession.swift @@ -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 + } +} diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index aad888ce2..fe74cf6d6 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -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() diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index fcf34b120..462821e55 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -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) diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift index d6e1495df..6d8dcd004 100644 --- a/Signal/src/call/PeerConnectionClient.swift +++ b/Signal/src/call/PeerConnectionClient.swift @@ -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() + } + } /** diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index e6bb67535..f12c847dd 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -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) { diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index 5f90c82f5..f3cb002ba 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -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) } diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index c78a7d482..a4034f679 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -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() { diff --git a/Signal/test/call/CallAudioSessionTest.swift b/Signal/test/call/CallAudioSessionTest.swift new file mode 100644 index 000000000..7e3e6d042 --- /dev/null +++ b/Signal/test/call/CallAudioSessionTest.swift @@ -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) + } +}