// _____ _
// |_ _| |_ _ _ ___ ___ _ __ __ _
// | | | ' \| '_/ -_) -_) ' \/ _` |_
// |_| |_||_|_| \___\___|_|_|_\__,_(_)
//
// Threema iOS Client
// Copyright (c) 2017-2020 Threema GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License, version 3,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
#import
#import
#import
#import
#import
#import "VoIPCallManager.h"
#import "VoIPSender.h"
#include
#import "VoIPCallIceCandidatesMessage.h"
#import "BundleUtil.h"
#import "CallViewController.h"
#import "CallManager.h"
#import "MainTabBarController.h"
#import "AppDelegate.h"
#import "UserSettings.h"
#import "EntityManager.h"
#import "DateFormatter.h"
#import "NSString+Hex.h"
#import "AppGroup.h"
#import "ServerConnector.h"
#import "DateFormatter.h"
#import "VoIPHelper.h"
#import "NotificationManager.h"
#import "PushSetting.h"
#import "ValidationLogger.h"
#import "Threema-Swift.h"
#define kIncomingCallTimeout 60.0
#define kLogStatsIntervalConnecting 2.0
#define kLogStatsIntervalConnected 30.0
@interface VoIPCallManager ()
@property (nonatomic, strong) RTCPeerConnectionFactory *factory;
@property (nonatomic, strong) RTCPeerConnection *connection;
@property (nonatomic, strong) Contact *contact;
@property (nonatomic, strong) EntityManager *entityManager;
@property (nonatomic, strong) NSMutableDictionary *bufferReceivedAddIceCandidates;
@property (nonatomic, strong) NSMutableDictionary *bufferReceivedRemoveIceCandidates;
@property (nonatomic, assign) BOOL isMuteEnabled;
@property (nonatomic, strong) RTCAudioTrack *defaultAudioTrack;
@property (nonatomic, strong) NSMutableArray *iceCandidates;
@property (nonatomic, strong) NSMutableArray *tmpIceCandidates;
@property (nonatomic) BOOL isCopyIceCandidates;
@property (nonatomic, strong) NSTimer *incomingCallTimer;
@property (nonatomic, strong) NSTimer *iceCandidatesTimer;
@property (strong, nonatomic) AVAudioPlayer *callPlayer;
@property (strong, nonatomic) AVAudioPlayer *hangupPlayer;
@property (strong, nonatomic) AVAudioPlayer *pickupPlayer;
@property (strong, nonatomic) AVAudioPlayer *ringTonePlayer;
@property (strong, nonatomic) AVAudioPlayer *problemPlayer;
@property (strong, nonatomic) AVAudioPlayer *rejectedPlayer;
@property (strong, nonatomic) NSTimer *durationTimer;
@property (strong, nonatomic) NSTimer *reconnectTimer;
@property (strong, nonatomic) NSTimer *statsTimer;
@property (nonatomic) BOOL isSpeakerActive;
@property (nonatomic) BOOL changedToWebRTCAudio;
@end
@implementation VoIPCallManager
+ (VoIPCallManager*)sharedVoIPCallManager {
static VoIPCallManager *instance;
@synchronized (self) {
if (!instance)
instance = [[VoIPCallManager alloc] init];
}
return instance;
}
- (id)init {
self = [super init];
if (self) {
_factory = [RTCPeerConnectionFactory new];
_bufferReceivedAddIceCandidates = [NSMutableDictionary new];
_bufferReceivedRemoveIceCandidates = [NSMutableDictionary new];
_isMuteEnabled = NO;
_state = VoIPCallManagerStateIdle;
_iceCandidates = [NSMutableArray new];
_tmpIceCandidates = [NSMutableArray new];
_isCopyIceCandidates = NO;
_entityManager = [[EntityManager alloc] init];
_isSpeakerActive = NO;
_changedToWebRTCAudio = NO;
_callAlreadyEnded = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]];
[[RTCAudioSession sharedInstance] addDelegate:self];
}
return self;
}
#pragma mark - private functions
- (RTCConfiguration *)defaultRTCConfiguration {
RTCConfiguration *configuration = [[RTCConfiguration alloc] init];
RTCIceServer *servers = [[RTCIceServer alloc] initWithURLStrings:@[@"turn:stun-voip.threema.ch:3478", @"turn:stun-voip.threema.ch:443", @"turn:stun-voip.threema.ch:53", @"turn:turn-voip.threema.ch:3478", @"turn:turn-voip.threema.ch:443", @"turn:turn-voip.threema.ch:53"] username:@"threema-voip-ios" credential:@"ZdDbP1PF1vpAnqWgHXNSag" tlsCertPolicy:RTCTlsCertPolicySecure];
configuration.iceServers = @[servers];
if (_contact.verificationLevel == kVerificationLevelUnverified || [UserSettings sharedUserSettings].alwaysRelayCalls) {
configuration.iceTransportPolicy = RTCIceTransportPolicyRelay;
}
configuration.bundlePolicy = RTCBundlePolicyMaxBundle;
configuration.rtcpMuxPolicy = RTCRtcpMuxPolicyRequire;
configuration.tcpCandidatePolicy = RTCTcpCandidatePolicyDisabled;
configuration.continualGatheringPolicy = RTCContinualGatheringPolicyGatherContinually;
return configuration;
}
- (RTCMediaConstraints *)defaultPeerConnectionConstraints {
NSDictionary *optionalConstraints = @{@"DtlsSrtpKeyAgreement": @"true"};
RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:optionalConstraints];
return constraints;
}
- (RTCMediaConstraints *)defaultOfferConstraints {
NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true", @"OfferToReceiveVideo": @"false"};
RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:nil];
return constraints;
}
- (RTCMediaConstraints *)defaultAnswerConstraints {
NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true", @"OfferToReceiveVideo": @"false"};
RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:nil];
return constraints;
}
- (RTCMediaConstraints *)defaultAudioConstraints {
RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:nil];
return constraints;
}
- (RTCMediaStream *)createLocalMediaStreamWithFactory:(RTCPeerConnectionFactory *)factory {
RTCAudioSource *source = [factory audioSourceWithConstraints:[self defaultAudioConstraints]];
RTCMediaStream *localStream = [factory mediaStreamWithStreamId:@"AMACALL"];
[localStream addAudioTrack:[factory audioTrackWithSource:source trackId:@"AMACALLa0"]];
return localStream;
}
+ (BOOL)isIPv6Address:(NSString *)ip {
const char *utf8 = [ip UTF8String];
// Check valid IPv4.
struct in_addr dst;
int success = inet_pton(AF_INET, utf8, &(dst.s_addr));
if (success != 1) {
// Check valid IPv6.
struct in6_addr dst6;
return inet_pton(AF_INET6, utf8, &dst6);
}
return NO;
}
- (BOOL)shouldAddCandidate:(RTCIceCandidate *)candidate {
BOOL addCandidate = NO;
if (![UserSettings sharedUserSettings].enableIPv6) {
NSArray *sdpSplit = [candidate.sdp componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" "]];
if (sdpSplit.count >= 5) {
if ([sdpSplit[4] rangeOfString:@"."].location == NSNotFound && [sdpSplit[4] rangeOfString:@":"].location == NSNotFound) {
addCandidate = YES;
} else {
if (![VoIPCallManager isIPv6Address:sdpSplit[4]]) {
addCandidate = YES;
}
}
} else {
addCandidate = YES;
}
} else {
addCandidate = YES;
}
return addCandidate;
}
- (void)updateCallDurationTime {
_callDurationTime = _callDurationTime + 1;
if (_state != VoIPCallManagerStateReconnecting) {
_callTimeString = [DateFormatter timeFormatted:_callDurationTime];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationCallInBackgroundTimeChanged object:[NSNumber numberWithInt:_callDurationTime]];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationCallInBackgroundTimeChanged object:nil];
}
}
- (void)setupCallTone {
NSString *soundFilePath = [BundleUtil pathForResource:@"ringing-tone-ch-fade" ofType:@"mp3"];
NSURL *filePath = [NSURL fileURLWithPath:soundFilePath];
_callPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:filePath error:nil];
_callPlayer.numberOfLoops = -1;
[_callPlayer prepareToPlay];
}
- (void)setupHangupTone {
NSString *soundFilePath = [BundleUtil pathForResource:@"threema_hangup" ofType:@"mp3"];
NSURL *filePath = [NSURL fileURLWithPath:soundFilePath];
_hangupPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:filePath error:nil];
_hangupPlayer.numberOfLoops = 1;
_hangupPlayer.delegate = self;
[_hangupPlayer prepareToPlay];
}
- (void)setupPickupTone {
NSString *soundFilePath = [BundleUtil pathForResource:@"threema_pickup" ofType:@"mp3"];
NSURL *filePath = [NSURL fileURLWithPath:soundFilePath];
_pickupPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:filePath error:nil];
_pickupPlayer.numberOfLoops = 1;
[_pickupPlayer prepareToPlay];
}
- (void)setupRingTone {
NSString *voIPSound = [UserSettings sharedUserSettings].voIPSound;
NSString *soundFilePath;
if (![voIPSound isEqualToString:@"default"]) {
soundFilePath = [BundleUtil pathForResource:[UserSettings sharedUserSettings].voIPSound ofType:@"mp3"];
} else {
soundFilePath = [BundleUtil pathForResource:@"threema_best" ofType:@"mp3"];
}
NSURL *filePath = [NSURL fileURLWithPath:soundFilePath];
_ringTonePlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:filePath error:nil];
_ringTonePlayer.numberOfLoops = -1;
[_ringTonePlayer prepareToPlay];
}
- (void)setupProblemTone {
NSString *soundFilePath = [BundleUtil pathForResource:@"threema_problem" ofType:@"mp3"];
NSURL *filePath = [NSURL fileURLWithPath:soundFilePath];
_problemPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:filePath error:nil];
_problemPlayer.numberOfLoops = -1;
[_problemPlayer prepareToPlay];
}
- (void)setupRejectedTone {
NSString *soundFilePath = [BundleUtil pathForResource:@"busy-4x" ofType:@"mp3"];
NSURL *filePath = [NSURL fileURLWithPath:soundFilePath];
_rejectedPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:filePath error:nil];
_rejectedPlayer.numberOfLoops = -1;
[_rejectedPlayer prepareToPlay];
}
- (void)playReconnecting:(NSTimer *)timer {
if (_state == VoIPCallManagerStateReconnecting) {
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:_state]];
[self playTone:VoIPCallManagerToneProblem];
AVAudioSession *session = [AVAudioSession sharedInstance];
NSArray *outputs = [[session currentRoute] outputs];
BOOL isSpeaker = NO;
for (AVAudioSessionPortDescription *desc in outputs) {
if ([desc.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
isSpeaker = YES;
}
}
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:nil];
isSpeaker ? [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil] : [session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
[session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
}
}
- (void)schedulePeriodStatsWithOptions:(VoIPStatsOptions *)options period:(NSTimeInterval)period {
// Reset timer
if (self.statsTimer && [self.statsTimer isValid]) {
[self.statsTimer invalidate];
self.statsTimer = nil;
}
// Create new timer with (but immediately log once)
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:self.connection forKey:@"connection"];
[dict setObject:options forKey:@"options"];
self.statsTimer = [NSTimer scheduledTimerWithTimeInterval:period target:self selector:@selector(logDebugStatsFromTimer:) userInfo:dict repeats:YES];
[self logDebugStats:dict];
[[NSRunLoop mainRunLoop] addTimer:self.statsTimer forMode:NSRunLoopCommonModes];
}
- (void)logDebugStatsFromTimer:(NSTimer *)timer {
NSDictionary *dict = [timer userInfo];
[self logDebugStats:dict];
}
- (void)logDebugStats:(NSDictionary *)dict {
RTCPeerConnection *connection = [dict objectForKey:@"connection"];
VoIPStatsOptions *options = [dict objectForKey:@"options"];
[connection statsForTrack:nil statsOutputLevel:RTCStatsOutputLevelDebug completionHandler:^(NSArray * _Nonnull report) {
VoIPStats *stats = [[VoIPStats alloc] initWithReport:report options:options];
NSString *statsStr = [stats getRepresentation];
[[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Call: Stats\n%@", statsStr]];
// Execute callback (if any)
void(^callback)(void) = [dict objectForKey:@"callback"];
if (callback) {
callback();
}
}];
}
- (NSString *)stringForIceConnectionState:(RTCIceConnectionState)state {
switch (state) {
case RTCIceConnectionStateNew:
return @"new";
case RTCIceConnectionStateChecking:
return @"checking";
case RTCIceConnectionStateConnected:
return @"connected";
case RTCIceConnectionStateCompleted:
return @"completed";
case RTCIceConnectionStateFailed:
return @"failed";
case RTCIceConnectionStateDisconnected:
return @"disconnected";
case RTCIceConnectionStateClosed:
return @"closed";
case RTCIceConnectionStateCount:
return @"count";
}
}
#pragma mark - public functions
- (Contact *)getCallContact {
return _contact;
}
- (void)startVoIPCallWithContact:(Contact *)contact {
_state = VoIPCallManagerStateWaitForRinging;
_changedToWebRTCAudio = NO;
[self setupTones];
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:_state]];
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:nil];
[session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
});
_contact = contact;
RTCMediaConstraints *constraints = [self defaultPeerConnectionConstraints];
RTCConfiguration *configuration = [self defaultRTCConfiguration];
_connection = [_factory peerConnectionWithConfiguration:configuration constraints:constraints delegate:self];
RTCMediaStream *localStream = [self createLocalMediaStreamWithFactory:_factory];
[_connection addStream:localStream];
[_connection offerForConstraints:constraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
[_connection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
if (!error) {
_isCallInitiator = YES;
[VoIPSender startVoIPCallWithContact:contact sessionDescription:sdp];
}
}];
}];
}
- (void)startRinging {
_state = VoIPCallManagerStateRinging;
[[VoIPCallManager sharedVoIPCallManager] playTone:VoIPCallManagerToneCall];
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:_state]];
}
- (void)startVoIPCallAnswerWithContact:(Contact *)contact {
_state = VoIPCallManagerStateInitializing;
_changedToWebRTCAudio = NO;
[self setupTones];
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:_state]];
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:nil];
[session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
});
__weak VoIPCallManager *weakSelf = self;
[[CallManager sharedInstance] callPickedUpFromReceiver];
if (!contact) {
contact = _contact;
}
RTCMediaConstraints *answerConstraints = [self defaultAnswerConstraints];
[_connection answerForConstraints:answerConstraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
if (!error) {
[_connection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
if (!error) {
_isCallInitiator = NO;
VoIPCallAnswerMessage *message = [VoIPCallAnswerMessage new];
message.answer = sdp;
message.action = VoIPCallAnswerMessageActionCall;
message.rejectReason = VoIPCallAnswerMessageRejectReasonUnknown;
[VoIPSender startVoIPCallAnswerWithContact:contact message:message];
if (weakSelf.incomingCallTimer && [weakSelf.incomingCallTimer isValid]){
[weakSelf.incomingCallTimer invalidate];
weakSelf.incomingCallTimer = nil;
}
}
}];
}
}];
}
- (void)setOffer:(RTCSessionDescription *)sdp fromContact:(Contact *)contact {
__weak VoIPCallManager *weakSelf = self;
_state = VoIPCallManagerStateWaitForRinging;
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:_state]];
});
_contact = contact;
RTCMediaConstraints *offerConstraints = [self defaultPeerConnectionConstraints];
RTCConfiguration *configuration = [self defaultRTCConfiguration];
_connection = [_factory peerConnectionWithConfiguration:configuration constraints:offerConstraints delegate:self];
RTCMediaStream *localStream = [self createLocalMediaStreamWithFactory:_factory];
[_connection addStream:localStream];
@synchronized (_bufferReceivedAddIceCandidates) {
[_bufferReceivedAddIceCandidates removeAllObjects];
}
@synchronized (_bufferReceivedRemoveIceCandidates) {
[_bufferReceivedRemoveIceCandidates removeAllObjects];
}
[_connection setRemoteDescription:sdp completionHandler:^(NSError * _Nullable error) {
if (!error) {
NSUUID *uuid = [NSUUID new];
_state = VoIPCallManagerStateRinging;
// add already received candidates
@synchronized (weakSelf.bufferReceivedAddIceCandidates) {
NSMutableArray *addCandidates = [weakSelf.bufferReceivedAddIceCandidates valueForKey:contact.identity];
[addCandidates enumerateObjectsUsingBlock:^(RTCIceCandidate *candidate, NSUInteger idx, BOOL * _Nonnull stop) {
[weakSelf.connection addIceCandidate:candidate];
}];
[weakSelf.bufferReceivedAddIceCandidates removeAllObjects];
}
@synchronized (weakSelf.bufferReceivedRemoveIceCandidates) {
NSMutableArray *removeCandidates = [weakSelf.bufferReceivedRemoveIceCandidates valueForKey:contact.identity];
if (removeCandidates) {
[weakSelf.connection removeIceCandidates:removeCandidates];
}
[weakSelf.bufferReceivedRemoveIceCandidates removeAllObjects];
}
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:weakSelf.state]];
_incomingCallTimer = [NSTimer scheduledTimerWithTimeInterval:kIncomingCallTimeout target:self selector:@selector(timeoutCall) userInfo:nil repeats:NO];
});
_isCallInitiator = NO;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0") && [UserSettings sharedUserSettings].enableCallKit && ![[[NSLocale currentLocale] objectForKey: NSLocaleCountryCode] isEqualToString:@"CN"]) {
[[CallManager sharedInstance] reportIncomingCallForUUID:uuid contact:contact];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
UIViewController *vc = [UIApplication sharedApplication].keyWindow.rootViewController;
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"CallStoryboard" bundle:nil];
CallViewController *callViewController = (CallViewController *)[storyboard instantiateInitialViewController];
callViewController.contact = [weakSelf contact];
callViewController.alreadyAccepted = NO;
callViewController.isCallInitiator = NO;
callViewController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[vc presentViewController:callViewController animated:NO completion:nil];
});
dispatch_async(dispatch_get_main_queue(), ^{
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) {
/* We're in the background and have received a message. There will be no push, so we need to generate a local notification */
NSString *cmd;
UNMutableNotificationContent *notification = [[UNMutableNotificationContent alloc] init];
notification.categoryIdentifier = @"INCOMCALL";
cmd = @"newcall";
NSString *voIPSound = [UserSettings sharedUserSettings].voIPSound;
if (![voIPSound isEqualToString:@"default"]) {
notification.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"%@.mp3", [UserSettings sharedUserSettings].voIPSound]];
} else {
notification.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"threema_best.mp3"]];
}
notification.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[NSDictionary dictionaryWithObjectsAndKeys: cmd, @"cmd", contact.displayName, @"from", nil], @"threema", nil];
notification.title = contact.displayName;
notification.body = NSLocalizedString(@"call_incoming_ended", @"");
NSString *notificationIdentifier = contact.identity;
UNNotificationRequest *notificationRequest = [UNNotificationRequest requestWithIdentifier:notificationIdentifier content:notification trigger:nil];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) {
}];
}
});
}
[VoIPSender sendVoIPCallRingingMessageToContact:weakSelf.contact];
}
}];
}
- (void)setAnswer:(RTCSessionDescription *)sdp {
__weak VoIPCallManager *weakSelf = self;
[_connection setRemoteDescription:sdp completionHandler:^(NSError * _Nullable error) {
if (error) {
[weakSelf callRejected];
}
}];
}
- (void)removeIceCandidates:(NSArray *)candidates fromIdentity:(NSString *)identity {
if (_connection) {
if ([_contact.identity isEqualToString:identity]) {
[_connection removeIceCandidates:candidates];
}
} else {
@synchronized (_bufferReceivedRemoveIceCandidates) {
NSMutableArray *candidatesArray = [_bufferReceivedRemoveIceCandidates valueForKey:identity];
if (!candidatesArray) {
candidatesArray = [NSMutableArray new];
}
[candidates enumerateObjectsUsingBlock:^(RTCIceCandidate *candidate, NSUInteger idx, BOOL * _Nonnull stop) {
[candidatesArray addObject:candidate];
}];
[_bufferReceivedRemoveIceCandidates setObject:candidatesArray forKey:identity];
}
}
}
- (void)addIceCandidates:(NSArray *)candidates fromIdentity:(NSString *)identity{
if (_connection) {
if ([_contact.identity isEqualToString:identity]) {
[candidates enumerateObjectsUsingBlock:^(RTCIceCandidate *candidate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([self shouldAddCandidate:candidate]) {
[_connection addIceCandidate:candidate];
}
}];
}
} else {
@synchronized (_bufferReceivedAddIceCandidates) {
NSMutableArray *candidatesArray = [_bufferReceivedAddIceCandidates valueForKey:identity];
if (!candidatesArray) {
candidatesArray = [NSMutableArray new];
}
[candidates enumerateObjectsUsingBlock:^(RTCIceCandidate *candidate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([self shouldAddCandidate:candidate]) {
[candidatesArray addObject:candidate];
}
}];
[_bufferReceivedAddIceCandidates setObject:candidatesArray forKey:identity];
}
}
}
- (void)hangup {
if (!_callAlreadyEnded) {
_callAlreadyEnded = YES;
[VoIPSender sendVoIPCallHangupToContact:_contact];
if (_state == VoIPCallManagerStateWaitForRinging || _state == VoIPCallManagerStateRinging || _state == VoIPCallManagerStateInitializing || _state == VoIPCallManagerStateCalling || _state == VoIPCallManagerStateReconnecting || _state == VoIPCallManagerStateSystemRejected) {
Conversation *conversation = [_entityManager conversationForContact:_contact createIfNotExisting:YES];
[_entityManager performSyncBlockAndSafe:^{
SystemMessage *systemMessage = [_entityManager.entityCreator systemMessageForConversation:conversation];
systemMessage.type = [NSNumber numberWithInteger:kSystemMessageCallEnded];
if (!_callTimeString)
_callTimeString = @"";
NSDictionary *argDict = @{@"DateString": [DateFormatter shortStyleTimeNoDate:[NSDate date]], @"CallTime": _callTimeString, @"CallInitiator": [NSNumber numberWithBool:_isCallInitiator]};
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:argDict options:NSJSONWritingPrettyPrinted error:&error];
systemMessage.arg = data;
systemMessage.isOwn = [NSNumber numberWithBool:_isCallInitiator];
systemMessage.conversation = conversation;
conversation.lastMessage = systemMessage;
_callTimeString = @"";
}];
}
if (_incomingCallTimer && [_incomingCallTimer isValid]){
[_incomingCallTimer invalidate];
_incomingCallTimer = nil;
}
[self disconnect: true];
[[CallManager sharedInstance] endCall];
}
}
- (void)hangupOnCompletion:(void(^)(void))onCompletion {
[VoIPSender sendVoIPCallHangupAndWaitToContact:_contact];
if (_state == VoIPCallManagerStateWaitForRinging || _state == VoIPCallManagerStateRinging || _state == VoIPCallManagerStateInitializing || _state == VoIPCallManagerStateCalling || _state == VoIPCallManagerStateReconnecting || _state == VoIPCallManagerStateSystemRejected) {
if (![[VoIPCallManager sharedVoIPCallManager] callAlreadyEnded]) {
[[VoIPCallManager sharedVoIPCallManager] setCallAlreadyEnded:YES];
Conversation *conversation = [_entityManager conversationForContact:_contact createIfNotExisting:YES];
[_entityManager performSyncBlockAndSafe:^{
SystemMessage *systemMessage = [_entityManager.entityCreator systemMessageForConversation:conversation];
systemMessage.type = [NSNumber numberWithInteger:kSystemMessageCallEnded];
if (!_callTimeString)
_callTimeString = @"";
NSDictionary *argDict = @{@"DateString": [DateFormatter shortStyleTimeNoDate:[NSDate date]], @"CallTime": _callTimeString, @"CallInitiator": [NSNumber numberWithBool:_isCallInitiator]};
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:argDict options:NSJSONWritingPrettyPrinted error:&error];
systemMessage.arg = data;
systemMessage.isOwn = [NSNumber numberWithBool:_isCallInitiator];
systemMessage.conversation = conversation;
conversation.lastMessage = systemMessage;
_callTimeString = @"";
}];
}
}
if (_incomingCallTimer && [_incomingCallTimer isValid]){
[_incomingCallTimer invalidate];
_incomingCallTimer = nil;
}
[self disconnect: true];
[[CallManager sharedInstance] endCall];
onCompletion();
}
- (void)callHangedup {
[self disconnect: true];
[[CallManager sharedInstance] endCall];
}
- (void)callRejected {
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:VoIPCallManagerStateSystemRejected]];
[_reconnectTimer invalidate];
[self setCallTimeString:@""];
[self disconnect: false];
[[CallManager sharedInstance] endCall];
}
- (void)timeoutCall {
__weak VoIPCallManager *weakSelf = self;
RTCMediaConstraints *answerConstraints = [self defaultAnswerConstraints];
[_connection answerForConstraints:answerConstraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
if (!error) {
VoIPCallAnswerMessage *message = [VoIPCallAnswerMessage new];
message.answer = sdp;
message.action = VoIPCallAnswerMessageActionReject;
message.rejectReason = VoIPCallAnswerMessageRejectReasonTimeout;
[VoIPSender startVoIPCallAnswerRejectWithContact:_contact message:message];
}
__block NSString *messageId;
Conversation *conversation = [_entityManager.entityFetcher conversationForIdentity:_contact.identity];
[_entityManager performSyncBlockAndSafe:^{
/* Insert system message to document the missed call */
NSDictionary *argDict;
SystemMessage *systemMessage = [_entityManager.entityCreator systemMessageForConversation:conversation];
systemMessage.type = [NSNumber numberWithInteger:kSystemMessageCallMissed];
systemMessage.isOwn = [NSNumber numberWithBool:NO];
conversation.unreadMessageCount = [NSNumber numberWithInt:[[conversation unreadMessageCount] intValue] + 1];
argDict = @{@"DateString": [DateFormatter shortStyleTimeNoDate:[NSDate date]], @"CallInitiator": [NSNumber numberWithBool:[[VoIPCallManager sharedVoIPCallManager] isCallInitiator]]};
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:argDict options:NSJSONWritingPrettyPrinted error:&error];
systemMessage.arg = data;
systemMessage.isOwn = [NSNumber numberWithBool:[[VoIPCallManager sharedVoIPCallManager] isCallInitiator]];
systemMessage.conversation = conversation;
conversation.lastMessage = systemMessage;
messageId = [NSString stringWithHexData:systemMessage.id];
}];
dispatch_async(dispatch_get_main_queue(),^{
[[NotificationManager sharedInstance] updateUnreadMessagesCount:NO];
});
__block UIApplicationState state;
dispatch_async(dispatch_get_main_queue(),^{
state = [UIApplication sharedApplication].applicationState;
});
PushSetting *pushSetting = [PushSetting findPushSettingForConversation:conversation];
BOOL canSendPush = YES;
if (pushSetting != nil) {
canSendPush = [pushSetting canSendPush];
}
if (state == UIApplicationStateBackground && canSendPush) {
UNMutableNotificationContent *notification = [[UNMutableNotificationContent alloc] init];
NSString *cmd;
if ([UserSettings sharedUserSettings].pushDecrypt) {
notification.title = _contact.displayName;
notification.body = NSLocalizedString(@"call_missed", nil);
notification.categoryIdentifier = @"CALL";
} else {
notification.body = [NSString stringWithFormat:NSLocalizedString(@"new_message_from_x", nil), _contact.displayName];
notification.categoryIdentifier = @"";
}
cmd = @"newmsg";
if (![[UserSettings sharedUserSettings].pushSound isEqualToString:@"none"]) {
if (pushSetting != nil) {
if (!pushSetting.silent) {
notification.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"%@.caf", [UserSettings sharedUserSettings].pushSound]];
}
} else {
notification.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"%@.caf", [UserSettings sharedUserSettings].pushSound]];
}
}
notification.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[NSDictionary dictionaryWithObjectsAndKeys: cmd, @"cmd", _contact.displayName, @"from", messageId, @"messageId", nil], @"threema", nil];
NSString *notificationIdentifier = _contact.identity;
UNNotificationRequest *notificationRequest = [UNNotificationRequest requestWithIdentifier:notificationIdentifier content:notification trigger:nil];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) {
}];
}
if (weakSelf.incomingCallTimer && [weakSelf.incomingCallTimer isValid]){
[weakSelf.incomingCallTimer invalidate];
weakSelf.incomingCallTimer = nil;
}
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:VoIPCallManagerStateIdle]];
[_reconnectTimer invalidate];
[self setCallTimeString:@""];
[[RTCAudioSession sharedInstance] lockForConfiguration];
NSError *rtcError = nil;
if (![[RTCAudioSession sharedInstance] setActive:NO error:&rtcError]) {
NSLog(@"resume music player failed, error=%@", rtcError);
}
[[RTCAudioSession sharedInstance] unlockForConfiguration];
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
if (![audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&rtcError]) {
NSLog(@"resume music player failed, error=%@", rtcError);
}
[self disconnect: true];
[[CallManager sharedInstance] endCall];
}];
}
- (void)rejectCallOnCompletion:(void(^)(void))onCompletion {
__weak VoIPCallManager *weakSelf = self;
RTCMediaConstraints *answerConstraints = [self defaultAnswerConstraints];
[_connection answerForConstraints:answerConstraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
if (!error) {
_isCallInitiator = NO;
VoIPCallAnswerMessage *message = [VoIPCallAnswerMessage new];
message.answer = sdp;
message.action = VoIPCallAnswerMessageActionReject;
message.rejectReason = VoIPCallAnswerMessageRejectReasonReject;
[VoIPSender startVoIPCallAnswerRejectWithContact:_contact message:message];
if (![[VoIPCallManager sharedVoIPCallManager] callAlreadyEnded]) {
[[VoIPCallManager sharedVoIPCallManager] setCallAlreadyEnded:YES];
Conversation *conversation = [_entityManager conversationForContact:_contact createIfNotExisting:YES];
[_entityManager performSyncBlockAndSafe:^{
SystemMessage *systemMessage = [_entityManager.entityCreator systemMessageForConversation:conversation];
systemMessage.type = [NSNumber numberWithInteger:kSystemMessageCallRejected];
NSDictionary *argDict = @{@"DateString": [DateFormatter shortStyleTimeNoDate:[NSDate date]], @"CallInitiator": [NSNumber numberWithBool:_isCallInitiator]};
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:argDict options:NSJSONWritingPrettyPrinted error:&error];
systemMessage.arg = data;
systemMessage.isOwn = [NSNumber numberWithBool:_isCallInitiator];
systemMessage.read = [NSNumber numberWithBool:YES];
systemMessage.conversation = conversation;
conversation.lastMessage = systemMessage;
}];
}
}
if (weakSelf.incomingCallTimer && [weakSelf.incomingCallTimer isValid]){
[weakSelf.incomingCallTimer invalidate];
weakSelf.incomingCallTimer = nil;
}
[self disconnect: false];
[[CallManager sharedInstance] endCall];
onCompletion();
}];
}
- (void)rejectCallWithBusy:(Contact *)contact onCompletion:(void(^)(void))onCompletion {
VoIPCallAnswerMessage *message = [VoIPCallAnswerMessage new];
message.answer = nil;
message.action = VoIPCallAnswerMessageActionReject;
message.rejectReason = VoIPCallAnswerMessageRejectReasonBusy;
[VoIPSender startVoIPCallAnswerRejectWithContact:contact message:message];
Conversation *conversation = [_entityManager conversationForContact:_contact createIfNotExisting:YES];
[_entityManager performSyncBlockAndSafe:^{
SystemMessage *systemMessage = [_entityManager.entityCreator systemMessageForConversation:conversation];
systemMessage.type = [NSNumber numberWithInteger:kSystemMessageCallMissed];
NSDictionary *argDict = @{@"DateString": [DateFormatter shortStyleTimeNoDate:[NSDate date]], @"CallInitiator": [NSNumber numberWithBool:NO]};
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:argDict options:NSJSONWritingPrettyPrinted error:&error];
systemMessage.arg = data;
systemMessage.isOwn = [NSNumber numberWithBool:NO];
systemMessage.conversation = conversation;
conversation.lastMessage = systemMessage;
conversation.unreadMessageCount = [NSNumber numberWithInt:[[conversation unreadMessageCount] intValue] + 1];
}];
dispatch_async(dispatch_get_main_queue(),^{
[[NotificationManager sharedInstance] updateUnreadMessagesCount:NO];
});
onCompletion();
}
- (void)rejectCallWithDisabled:(Contact *)contact onCompletion:(void(^)(void))onCompletion {
_state = VoIPCallManagerStateSystemRejected;
VoIPCallAnswerMessage *message = [VoIPCallAnswerMessage new];
message.answer = nil;
message.action = VoIPCallAnswerMessageActionReject;
message.rejectReason = VoIPCallAnswerMessageRejectReasonDisabled;
[VoIPSender startVoIPCallAnswerRejectWithContact:contact message:message];
dispatch_async(dispatch_get_main_queue(),^{
[[NotificationManager sharedInstance] updateUnreadMessagesCount:NO];
});
[[CallManager sharedInstance] endCall];
[self disconnect: false];
if (onCompletion != nil) {
onCompletion();
}
}
- (void)disconnect:(BOOL)playSound {
void(^disconnectCallback)(void) = ^{
[_connection close];
_connection = nil;
};
if (playSound) {
[self playTone:VoIPCallManagerToneHangup];
}
[[VoIPHelper shared] setIsCallActiveInBackground:NO];
[[VoIPHelper shared] setContactName:nil];
[[VoIPHelper shared] setLastUpdatedCallDuration:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationCallInBackground object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationCallInBackgroundTimeChanged object:nil];
if (_connection) {
dispatch_async(dispatch_get_main_queue(),^{
if (_statsTimer && [_statsTimer isValid]) {
// Invalidate timer
[_statsTimer invalidate];
_statsTimer = nil;
// Hijack the existing dict, override options and set callback
VoIPStatsOptions *options = [[VoIPStatsOptions alloc] init];
options.transport = true;
options.inboundRtp = true;
options.codecs = true;
options.candidatePairsFlag = CandidatePairVariantOVERVIEW_AND_DETAILED;
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
self.connection, @"connection",
options, @"options",
disconnectCallback, @"callback",
nil];
// One-shot stats fetch before disconnect
[self logDebugStats:dict];
} else {
disconnectCallback();
}
[_incomingCallTimer invalidate];
_incomingCallTimer = nil;
_contact = nil;
_callAlreadyEnded = NO;
_isMuteEnabled = NO;
[_iceCandidatesTimer invalidate];
_iceCandidatesTimer = nil;
[_callDurationTimer invalidate];
_callDurationTimer = nil;
_callDurationTime = 0;
_callTimeString = @"";
[_iceCandidates removeAllObjects];
_state = VoIPCallManagerStateIdle;
});
}
}
- (void)muteAudioIn {
RTCMediaStream *localStream = _connection.localStreams[0];
self.defaultAudioTrack = localStream.audioTracks[0];
[localStream removeAudioTrack:localStream.audioTracks[0]];
[_connection removeStream:localStream];
[_connection addStream:localStream];
_isMuteEnabled = YES;
}
- (void)unmuteAudioIn {
RTCMediaStream* localStream = _connection.localStreams[0];
[localStream addAudioTrack:self.defaultAudioTrack];
[_connection removeStream:localStream];
[_connection addStream:localStream];
_isMuteEnabled = NO;
}
- (BOOL)isMuteEnabled {
return _isMuteEnabled;
}
- (void)invalidateDurationTimer {
[_durationTimer invalidate];
_durationTimer = nil;
}
- (void)setupTones {
[self setupCallTone];
[self setupHangupTone];
[self setupPickupTone];
[self setupRingTone];
[self setupProblemTone];
[self setupRejectedTone];
}
- (void)playTone:(VoIPCallManagerTone)tone {
switch (tone) {
case VoIPCallManagerToneCall:
[_hangupPlayer stop];
[_pickupPlayer stop];
[_ringTonePlayer stop];
[_problemPlayer stop];
[_rejectedPlayer stop];
[_callPlayer setCurrentTime:0];
[_callPlayer play];
break;
case VoIPCallManagerToneHangup:
[_callPlayer stop];
[_pickupPlayer stop];
[_ringTonePlayer stop];
[_problemPlayer stop];
[_rejectedPlayer stop];
[_hangupPlayer play];
break;
case VoIPCallManagerTonePickup:
[_callPlayer stop];
[_hangupPlayer stop];
[_ringTonePlayer stop];
[_problemPlayer stop];
[_rejectedPlayer stop];
[_pickupPlayer play];
break;
case VoIPCallManagerToneRing:
[_callPlayer stop];
[_hangupPlayer stop];
[_pickupPlayer stop];
[_problemPlayer stop];
[_rejectedPlayer stop];
[_ringTonePlayer setCurrentTime:0];
[_ringTonePlayer play];
break;
case VoIPCallManagerToneProblem:
[_callPlayer stop];
[_hangupPlayer stop];
[_pickupPlayer stop];
[_ringTonePlayer stop];
[_rejectedPlayer stop];
[_problemPlayer setCurrentTime:0];
[_problemPlayer play];
break;
case VoIPCallManagerToneRejected:
[_callPlayer stop];
[_hangupPlayer stop];
[_pickupPlayer stop];
[_ringTonePlayer stop];
[_problemPlayer stop];
[_rejectedPlayer setCurrentTime:0];
[_rejectedPlayer play];
default:
break;
}
}
- (void)stopAllTones {
[_callPlayer stop];
[_hangupPlayer stop];
[_pickupPlayer stop];
[_ringTonePlayer stop];
[_problemPlayer stop];
[_rejectedPlayer stop];
[[RTCAudioSession sharedInstance] lockForConfiguration];
NSError *error = nil;
if (![[RTCAudioSession sharedInstance] setActive:NO error:&error]) {
NSLog(@"resume music player failed, error=%@", error);
}
[[RTCAudioSession sharedInstance] unlockForConfiguration];
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
if (![audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error]) {
NSLog(@"resume music player failed, error=%@", error);
}
}
- (void)activateRTCAudio {
AVAudioSessionRouteDescription *currentRoute = [[RTCAudioSession sharedInstance] currentRoute];
AVAudioSessionPortDescription *portDesc = [[currentRoute outputs] firstObject];
if ([portDesc.portType isEqualToString:@"Speaker"]) {
NSError *error;
[[RTCAudioSession sharedInstance] lockForConfiguration];
[[RTCAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[[RTCAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error];
[[RTCAudioSession sharedInstance] setActive:YES error:&error];
[[RTCAudioSession sharedInstance] unlockForConfiguration];
[[RTCAudioSession sharedInstance] lockForConfiguration];
[[RTCAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[[RTCAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
[[RTCAudioSession sharedInstance] setActive:YES error:&error];
[[RTCAudioSession sharedInstance] unlockForConfiguration];
} else {
NSError *error;
[[RTCAudioSession sharedInstance] lockForConfiguration];
[[RTCAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[[RTCAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
[[RTCAudioSession sharedInstance] setActive:YES error:&error];
[[RTCAudioSession sharedInstance] unlockForConfiguration];
[[RTCAudioSession sharedInstance] lockForConfiguration];
[[RTCAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[[RTCAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error];
[[RTCAudioSession sharedInstance] setActive:YES error:&error];
[[RTCAudioSession sharedInstance] unlockForConfiguration];
}
}
#pragma mark - RTCPeerConnectionDelegate
- (void)peerConnection:(RTCPeerConnection *)peerConnection didChangeSignalingState:(RTCSignalingState)stateChanged {
}
/** Called when media is received on a new stream from remote peer. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didAddStream:(RTCMediaStream *)stream {
// ignore
}
/** Called when a remote peer closes a stream. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didRemoveStream:(RTCMediaStream *)stream {
// ignore
}
/** Called when negotiation is needed, for example ICE has restarted. */
- (void)peerConnectionShouldNegotiate:(RTCPeerConnection *)peerConnection {
// ignore
}
/** Called any time the IceConnectionState changes. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didChangeIceConnectionState:(RTCIceConnectionState)newState {
BOOL wasInitializing = _state == VoIPCallManagerStateInitializing;
NSString *strState = [self stringForIceConnectionState:newState];
[[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat: @"Call: ICE connection state -> %@", strState]];
if (newState == RTCIceConnectionStateChecking) {
_state = VoIPCallManagerStateInitializing;
[self playTone:VoIPCallManagerTonePickup];
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:nil];
[session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
dispatch_async(dispatch_get_main_queue(),^{
// Schedule 'connecting' stats timer
VoIPStatsOptions *options = [[VoIPStatsOptions alloc] init];
options.transport = true;
options.inboundRtp = true;
options.codecs = true;
options.candidatePairsFlag = CandidatePairVariantOVERVIEW_AND_DETAILED;
[self schedulePeriodStatsWithOptions:options period:kLogStatsIntervalConnecting];
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStartDebugMode object:_connection];
});
} else if (newState == RTCIceConnectionStateConnected) {
if (_state != VoIPCallManagerStateReconnecting) {
[_callDurationTimer invalidate];
_callDurationTimer = nil;
_callDurationTime = 0;
dispatch_async(dispatch_get_main_queue(),^{
self.callDurationTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateCallDurationTime) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.callDurationTimer forMode:NSRunLoopCommonModes];
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStartDebugMode object:_connection];
});
}
_state = VoIPCallManagerStateCalling;
[_callPlayer stop];
[_hangupPlayer stop];
[_ringTonePlayer stop];
[_problemPlayer stop];
_changedToWebRTCAudio = NO;
} else if (newState == RTCIceConnectionStateCompleted) {
_state = VoIPCallManagerStateCalling;
[_callPlayer stop];
[_hangupPlayer stop];
[_ringTonePlayer stop];
[_problemPlayer stop];
if (_iceCandidatesTimer && [_iceCandidatesTimer isValid]){
[_iceCandidatesTimer invalidate];
_iceCandidatesTimer = nil;
}
} else if (newState == RTCIceConnectionStateFailed) {
[self hangup];
} else if (newState == RTCIceConnectionStateDisconnected) {
_state = VoIPCallManagerStateReconnecting;
dispatch_async(dispatch_get_main_queue(),^{
// wait 2 seconds and play sound if its still on status reconnecting
_reconnectTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(playReconnecting:) userInfo:nil repeats:NO];
});
} else if (newState == RTCIceConnectionStateClosed) {
}
if (newState != RTCIceConnectionStateDisconnected) {
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:kVoIPCallStatusChanged object:[NSNumber numberWithInt:_state]];
});
}
if (wasInitializing && (newState == RTCIceConnectionStateConnected || newState == RTCIceConnectionStateCompleted)) {
dispatch_async(dispatch_get_main_queue(),^{
// Schedule 'connected' stats timer
VoIPStatsOptions *options = [[VoIPStatsOptions alloc] init];
options.transport = true;
options.selectedCandidatePair = true;
options.inboundRtp = true;
options.codecs = true;
options.candidatePairsFlag = CandidatePairVariantOVERVIEW;
[self schedulePeriodStatsWithOptions:options period:kLogStatsIntervalConnected];
});
}
}
/** Called any time the IceGatheringState changes. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didChangeIceGatheringState:(RTCIceGatheringState)newState {
// ignore
}
/** New ice candidate has been found. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate {
if ([self shouldAddCandidate:candidate]) {
if (_isCopyIceCandidates) {
[_tmpIceCandidates addObject:candidate];
} else {
if (_tmpIceCandidates.count > 0) {
[_iceCandidates addObjectsFromArray:_tmpIceCandidates];
[_tmpIceCandidates removeAllObjects];
}
[_iceCandidates addObject:candidate];
}
dispatch_async(dispatch_get_main_queue(),^{
if (!_iceCandidatesTimer) {
_iceCandidatesTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(sendCandidates:) userInfo:nil repeats:YES];
}
});
}
}
- (void)sendCandidates:(NSTimer *)timer {
if (_iceCandidates.count) {
_isCopyIceCandidates = YES;
NSMutableArray *candidates = _iceCandidates.copy;
[_iceCandidates removeAllObjects];
_isCopyIceCandidates = NO;
NSMutableArray *sendCandidates = [NSMutableArray new];
int candidatesCount = 0;
for (int i = 0; i < candidates.count; i++) {
candidatesCount++;
[sendCandidates addObject:candidates[i]];
if (candidates.count > 0 && (candidatesCount == 5 || i == candidates.count - 1)) {
VoIPCallIceCandidatesMessage *message = [VoIPCallIceCandidatesMessage new];
message.removed = NO;
message.candidates = sendCandidates.copy;
[VoIPSender sendVoIPCallIceCandidatesMessage:message toContact:_contact];
[sendCandidates removeAllObjects];
candidatesCount = 0;
}
}
}
}
/** Called when a group of local Ice candidates have been removed. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didRemoveIceCandidates:(NSArray *)candidates {
// send it only all 50 milli seconds
VoIPCallIceCandidatesMessage *message = [VoIPCallIceCandidatesMessage new];
message.removed = YES;
message.candidates = candidates;
[VoIPSender sendVoIPCallIceCandidatesMessage:message toContact:_contact];
}
/** New data channel has been opened. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didOpenDataChannel:(RTCDataChannel *)dataChannel {
// ignore
}
#pragma mark - AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
[[RTCAudioSession sharedInstance] lockForConfiguration];
NSError *error = nil;
if (![[RTCAudioSession sharedInstance] setActive:NO error:&error]) {
NSLog(@"resume music player failed, error=%@", error);
}
[[RTCAudioSession sharedInstance] unlockForConfiguration];
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
if (![audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error]) {
NSLog(@"resume music player failed, error=%@", error);
}
}
#pragma mark - RTCAudioSessionDelegate
- (void)audioSessionDidStartPlayOrRecord:(RTCAudioSession *)session {
if ([session.currentRoute.outputs[0].portType isEqualToString:@"Speaker"]) {
_isSpeakerActive = YES;
} else {
_isSpeakerActive = NO;
}
_changedToWebRTCAudio = YES;
}
- (void)audioSessionDidStopPlayOrRecord:(RTCAudioSession *)session {
_changedToWebRTCAudio = NO;
}
#pragma mark - Notifications
- (void)handleRouteChange:(NSNotification *)notification {
if (_changedToWebRTCAudio && _isSpeakerActive) {
NSError *error;
[[RTCAudioSession sharedInstance] lockForConfiguration];
[[RTCAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[[RTCAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
[[RTCAudioSession sharedInstance] setActive:YES error:&error];
[[RTCAudioSession sharedInstance] unlockForConfiguration];
}
}
@end