/
MXJingleCallStackCall.m
609 lines (487 loc) · 19 KB
/
MXJingleCallStackCall.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
/*
Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXJingleCallStackCall.h"
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
#import "MXTools.h"
#import "MXJingleVideoView.h"
#import "MXJingleCameraCaptureController.h"
#import <WebRTC/WebRTC.h>
@interface MXJingleCallStackCall () <RTCPeerConnectionDelegate>
{
/**
The libjingle all purpose factory.
*/
RTCPeerConnectionFactory *peerConnectionFactory;
/**
The libjingle object handling the call.
*/
RTCPeerConnection *peerConnection;
/**
The media tracks.
*/
RTCAudioTrack *localAudioTrack;
RTCVideoTrack *localVideoTrack;
RTCVideoTrack *remoteVideoTrack;
/**
The view that displays the remote video.
*/
MXJingleVideoView *remoteJingleVideoView;
/**
Flag indicating if this is a video call.
*/
BOOL isVideoCall;
/**
Success block for the async `startCapturingMediaWithVideo` method.
*/
void (^onStartCapturingMediaWithVideoSuccess)(void);
}
@property (nonatomic, strong) RTCVideoCapturer *videoCapturer;
@property (nonatomic, strong) MXJingleCameraCaptureController *captureController;
@end
@implementation MXJingleCallStackCall
@synthesize selfVideoView, remoteVideoView, audioToSpeaker, cameraPosition, delegate;
- (instancetype)initWithFactory:(RTCPeerConnectionFactory *)factory
{
self = [super init];
if (self)
{
peerConnectionFactory = factory;
cameraPosition = AVCaptureDevicePositionFront;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleRouteChangeNotification:)
name:AVAudioSessionRouteChangeNotification
object:nil];
}
return self;
}
- (void)startCapturingMediaWithVideo:(BOOL)video success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
onStartCapturingMediaWithVideoSuccess = success;
isVideoCall = video;
// Video requires views to render to before calling createLocalMediaStream
if (!video || (selfVideoView && remoteVideoView))
{
[self createLocalMediaStream];
}
else
{
NSLog(@"[MXJingleCallStackCall] Wait for the setting of selfVideoView and remoteVideoView before calling createLocalMediaStream");
}
}
- (void)end
{
self.videoCapturer = nil;
[self.captureController stopCapture];
self.captureController = nil;
[peerConnection close];
peerConnection = nil;
// Reset RTC tracks, a latency was observed on avFoundationVideoSourceWithConstraints call when localVideoTrack was not reseted.
localAudioTrack = nil;
localVideoTrack = nil;
remoteVideoTrack = nil;
self.selfVideoView = nil;
self.remoteVideoView = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)addTURNServerUris:(NSArray<NSString *> *)uris withUsername:(nullable NSString *)username password:(nullable NSString *)password
{
RTCIceServer *ICEServer = [[RTCIceServer alloc] initWithURLStrings:uris
username:username
credential:password];
if (!ICEServer)
{
NSLog(@"[MXJingleCallStackCall] addTURNServerUris: Warning: Failed to create RTCICEServer with credentials %@: %@ for:\n%@", username, password, uris);
}
if (ICEServer)
{
RTCMediaConstraints *constraints =
[[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil
optionalConstraints:@{
@"RtpDataChannels": @"true"
}];
RTCConfiguration *configuration = [[RTCConfiguration alloc] init];
configuration.iceServers = @[ICEServer];
// The libjingle call object can now be created
peerConnection = [peerConnectionFactory peerConnectionWithConfiguration:configuration constraints:constraints delegate:self];
}
}
- (void)handleRemoteCandidate:(NSDictionary<NSString *, NSObject *> *)candidate
{
RTCIceCandidate *iceCandidate = [[RTCIceCandidate alloc] initWithSdp:(NSString *)candidate[@"candidate"]
sdpMLineIndex:[(NSNumber *)candidate[@"sdpMLineIndex"] intValue]
sdpMid:(NSString *)candidate[@"sdpMid"]];
[peerConnection addIceCandidate:iceCandidate];
}
#pragma mark - Incoming call
- (void)handleOffer:(NSString *)sdpOffer success:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
RTCSessionDescription *sessionDescription = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdpOffer];
[peerConnection setRemoteDescription:sessionDescription completionHandler:^(NSError * _Nullable error) {
NSLog(@"[MXJingleCallStackCall] setRemoteDescription: error: %@", error);
// Return on main thread
dispatch_async(dispatch_get_main_queue(), ^{
if (!error)
{
success();
}
else
{
failure(error);
}
});
}];
}
- (void)createAnswer:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
{
MXWeakify(self);
[peerConnection answerForConstraints:self.mediaConstraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
MXStrongifyAndReturnIfNil(self);
if (!error)
{
// Report this sdp back to libjingle
[self->peerConnection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
// Return on main thread
dispatch_async(dispatch_get_main_queue(), ^{
if (!error)
{
success(sdp.sdp);
}
else
{
failure(error);
}
});
}];
}
else
{
// Return on main thread
dispatch_async(dispatch_get_main_queue(), ^{
failure(error);
});
}
}];
}
#pragma mark - Outgoing call
- (void)createOffer:(void (^)(NSString *sdp))success failure:(void (^)(NSError *))failure
{
MXWeakify(self);
[peerConnection offerForConstraints:self.mediaConstraints completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
MXStrongifyAndReturnIfNil(self);
if (!error)
{
// Report this sdp back to libjingle
[self->peerConnection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
// Return on main thread
dispatch_async(dispatch_get_main_queue(), ^{
if (!error)
{
success(sdp.sdp);
}
else
{
failure(error);
}
});
}];
}
else
{
// Return on main thread
dispatch_async(dispatch_get_main_queue(), ^{
failure(error);
});
}
}];
}
- (void)handleAnswer:(NSString *)sdp success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
RTCSessionDescription *sessionDescription = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeAnswer sdp:sdp];
[peerConnection setRemoteDescription:sessionDescription completionHandler:^(NSError * _Nullable error) {
// Return on main thread
dispatch_async(dispatch_get_main_queue(), ^{
if (!error)
{
success();
}
else
{
failure(error);
}
});
}];
}
#pragma mark - RTCPeerConnectionDelegate delegate
// Triggered when the SignalingState changed.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didChangeSignalingState:(RTCSignalingState)stateChanged
{
NSLog(@"[MXJingleCallStackCall] didChangeSignalingState: %tu", stateChanged);
}
// Triggered when media is received on a new stream from remote peer.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didAddStream:(RTCMediaStream *)stream
{
NSLog(@"[MXJingleCallStackCall] didAddStream");
// This is mandatory to keep a reference on the video track
// Else the video does not display in self.remoteVideoView
remoteVideoTrack = stream.videoTracks.lastObject;
if (remoteVideoTrack)
{
MXWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
MXStrongifyAndReturnIfNil(self);
// Use self.remoteVideoView as a container of a RTCEAGLVideoView
self->remoteJingleVideoView = [[MXJingleVideoView alloc] initWithContainerView:self.remoteVideoView];
[self->remoteVideoTrack addRenderer:self->remoteJingleVideoView];
});
}
}
// Triggered when a remote peer close a stream.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didRemoveStream:(RTCMediaStream *)stream
{
NSLog(@"[MXJingleCallStackCall] didRemoveStream");
}
// Triggered when renegotiation is needed, for example the ICE has restarted.
- (void)peerConnectionShouldNegotiate:(RTCPeerConnection *)peerConnection
{
NSLog(@"[MXJingleCallStackCall] peerConnectionShouldNegotiate");
}
// Called any time the ICEConnectionState changes.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didChangeIceConnectionState:(RTCIceConnectionState)newState
{
NSLog(@"[MXJingleCallStackCall] didChangeIceConnectionState: %@", @(newState));
switch (newState)
{
case RTCIceConnectionStateConnected:
{
// WebRTC has the given sequence of state changes for outgoing calls
// RTCIceConnectionStateConnected -> RTCIceConnectionStateCompleted -> RTCIceConnectionStateConnected
// Make sure you handle this situation right. For example check if the call is in the connecting state
// before starting react on this message
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate callStackCallDidConnect:self];
});
break;
}
case RTCIceConnectionStateFailed:
{
// ICE discovery has failed or the connection has dropped
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate callStackCall:self onError:nil];
});
break;
}
default:
break;
}
}
// Called any time the ICEGatheringState changes.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didChangeIceGatheringState:(RTCIceGatheringState)newState
{
NSLog(@"[MXJingleCallStackCall] didChangeIceGatheringState: %@", @(newState));
}
// New Ice candidate have been found.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didGenerateIceCandidate:(RTCIceCandidate *)candidate
{
NSLog(@"[MXJingleCallStackCall] didGenerateIceCandidate: %@", candidate);
// Forward found ICE candidates
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate callStackCall:self onICECandidateWithSdpMid:candidate.sdpMid sdpMLineIndex:candidate.sdpMLineIndex candidate:candidate.sdp];
});
}
// Called when a group of local Ice candidates have been removed.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didRemoveIceCandidates:(NSArray<RTCIceCandidate *> *)candidates;
{
NSLog(@"[MXJingleCallStackCall] didRemoveIceCandidates");
}
// New data channel has been opened.
- (void)peerConnection:(RTCPeerConnection*)peerConnection
didOpenDataChannel:(RTCDataChannel*)dataChannel
{
NSLog(@"[MXJingleCallStackCall] didOpenDataChannel");
}
#pragma mark - Properties
- (void)setSelfVideoView:(nullable UIView *)selfVideoView2
{
selfVideoView = selfVideoView2;
[self checkStartGetCaptureSourcesForVideo];
}
- (void)setRemoteVideoView:(nullable UIView *)remoteVideoView2
{
remoteVideoView = remoteVideoView2;
[self checkStartGetCaptureSourcesForVideo];
}
- (UIDeviceOrientation)selfOrientation
{
// @TODO: Hmm
return UIDeviceOrientationUnknown;
}
- (void)setSelfOrientation:(UIDeviceOrientation)selfOrientation
{
// Force recomputing of the remote video aspect ratio
[remoteJingleVideoView setNeedsLayout];
}
- (BOOL)audioMuted
{
return !localAudioTrack.isEnabled;
}
- (void)setAudioMuted:(BOOL)audioMuted
{
localAudioTrack.isEnabled = !audioMuted;
}
- (BOOL)videoMuted
{
return !localVideoTrack.isEnabled;
}
- (void)setVideoMuted:(BOOL)videoMuted
{
localVideoTrack.isEnabled = !videoMuted;
}
- (void)setAudioToSpeaker:(BOOL)theAudioToSpeaker
{
audioToSpeaker = theAudioToSpeaker;
if (audioToSpeaker)
{
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
}
else
{
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
}
}
- (void)setCameraPosition:(AVCaptureDevicePosition)theCameraPosition
{
cameraPosition = theCameraPosition;
if (localVideoTrack)
{
[self fixMirrorOnSelfVideoView];
}
self.captureController.cameraPosition = theCameraPosition;
}
#pragma mark - Private methods
- (RTCMediaConstraints *)mediaConstraints
{
return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:@{
@"OfferToReceiveAudio": @"true",
@"OfferToReceiveVideo": (isVideoCall ? @"true" : @"false")
}
optionalConstraints:nil];
}
- (void)createLocalMediaStream
{
// Set up audio
localAudioTrack = [self createLocalAudioTrack];
[peerConnection addTrack:localAudioTrack streamIds:@[@"ARDAMS"]];
// And video
if (isVideoCall)
{
localVideoTrack = [self createLocalVideoTrack];
// Create a video track and add it to the media stream
if (localVideoTrack)
{
[peerConnection addTrack:localVideoTrack streamIds:@[@"ARDAMS"]];
// Display the self view
// Use selfVideoView as a container of a RTCEAGLVideoView
MXJingleVideoView *renderView = [[MXJingleVideoView alloc] initWithContainerView:self.selfVideoView];
[self startVideoCaptureWithRenderer:renderView];
}
}
// Set the audio route
self.audioToSpeaker = audioToSpeaker;
if (onStartCapturingMediaWithVideoSuccess)
{
onStartCapturingMediaWithVideoSuccess();
onStartCapturingMediaWithVideoSuccess = nil;
}
}
- (RTCAudioTrack*)createLocalAudioTrack
{
RTCMediaConstraints *mediaConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:nil];
RTCAudioSource *localAudioSource = [peerConnectionFactory audioSourceWithConstraints:mediaConstraints];
return [peerConnectionFactory audioTrackWithSource:localAudioSource trackId:@"ARDAMSa0"];
}
- (RTCVideoTrack*)createLocalVideoTrack
{
RTCVideoSource *localVideoSource = [peerConnectionFactory videoSource];
self.videoCapturer = [self createVideoCapturerWithVideoSource:localVideoSource];
return [peerConnectionFactory videoTrackWithSource:localVideoSource trackId:@"ARDAMSv0"];
}
- (RTCVideoCapturer*)createVideoCapturerWithVideoSource:(RTCVideoSource*)videoSource
{
RTCVideoCapturer *videoCapturer;
#if !TARGET_OS_SIMULATOR
videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];
#endif
return videoCapturer;
}
- (void)startVideoCaptureWithRenderer:(id<RTCVideoRenderer>)videoRenderer
{
if (!self.videoCapturer || ![self.videoCapturer isKindOfClass:[RTCCameraVideoCapturer class]])
{
return;
}
RTCCameraVideoCapturer *cameraVideoCapturer = (RTCCameraVideoCapturer*)self.videoCapturer;
self.captureController = [[MXJingleCameraCaptureController alloc] initWithCapturer:cameraVideoCapturer];
[localVideoTrack addRenderer:videoRenderer];
[self.captureController startCapture];
}
- (void)checkStartGetCaptureSourcesForVideo
{
if (onStartCapturingMediaWithVideoSuccess && selfVideoView && remoteVideoView)
{
NSLog(@"[MXJingleCallStackCall] selfVideoView and remoteVideoView are set. Call createLocalMediaStream");
[self createLocalMediaStream];
}
}
- (void)fixMirrorOnSelfVideoView
{
if (cameraPosition == AVCaptureDevicePositionFront)
{
// Apply a left to right flip on the self video view on the front camera preview
// so that the user sees himself as in a mirror
selfVideoView.transform = CGAffineTransformMakeScale(-1.0, 1.0);
}
else
{
selfVideoView.transform = CGAffineTransformIdentity;
}
}
- (void)handleRouteChangeNotification:(NSNotification *)notification
{
AVAudioSessionRouteChangeReason changeReason = [notification.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
if (changeReason == AVAudioSessionRouteChangeReasonCategoryChange)
{
// WebRTC sets AVAudioSession's category right before call starts, this can lead to changing output route
// which user selected when the call was in connecting state.
// So we need to perform additional checks and override ouput port if needed
AVAudioSessionRouteDescription *currentRoute = [[AVAudioSession sharedInstance] currentRoute];
BOOL isOutputSpeaker = [currentRoute.outputs.firstObject.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker];
if (audioToSpeaker && !isOutputSpeaker)
{
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
}
else if (!audioToSpeaker && isOutputSpeaker)
{
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
}
}
}
@end