summaryrefslogtreecommitdiff
path: root/app/webrtc/objctests/RTCPeerConnectionTest.mm
blob: 92d3c4904c68f84dd3cc80cc43aa050144359f6e (plain)
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
/*
 * libjingle
 * Copyright 2013, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#import <Foundation/Foundation.h>

#import "RTCICEServer.h"
#import "RTCMediaConstraints.h"
#import "RTCMediaStream.h"
#import "RTCPair.h"
#import "RTCPeerConnection.h"
#import "RTCPeerConnectionFactory.h"
#import "RTCPeerConnectionSyncObserver.h"
#import "RTCSessionDescription.h"
#import "RTCSessionDescriptionSyncObserver.h"
#import "RTCVideoRenderer.h"
#import "RTCVideoTrack.h"

#include "webrtc/base/gunit.h"
#include "webrtc/base/ssladapter.h"

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

@interface RTCFakeRenderer : NSObject <RTCVideoRenderer>
@end

@implementation RTCFakeRenderer

- (void)setSize:(CGSize)size {}
- (void)renderFrame:(RTCI420Frame*)frame {}

@end

@interface RTCPeerConnectionTest : NSObject

// Returns whether the two sessions are of the same type.
+ (BOOL)isSession:(RTCSessionDescription*)session1
    ofSameTypeAsSession:(RTCSessionDescription*)session2;

// Create and add tracks to pc, with the given source, label, and IDs
- (RTCMediaStream*)addTracksToPeerConnection:(RTCPeerConnection*)pc
                                 withFactory:(RTCPeerConnectionFactory*)factory
                                 videoSource:(RTCVideoSource*)videoSource
                                 streamLabel:(NSString*)streamLabel
                                videoTrackID:(NSString*)videoTrackID
                                audioTrackID:(NSString*)audioTrackID;

- (void)testCompleteSessionWithFactory:(RTCPeerConnectionFactory*)factory;

@end

@implementation RTCPeerConnectionTest

+ (BOOL)isSession:(RTCSessionDescription*)session1
    ofSameTypeAsSession:(RTCSessionDescription*)session2 {
  return [session1.type isEqual:session2.type];
}

- (RTCMediaStream*)addTracksToPeerConnection:(RTCPeerConnection*)pc
                                 withFactory:(RTCPeerConnectionFactory*)factory
                                 videoSource:(RTCVideoSource*)videoSource
                                 streamLabel:(NSString*)streamLabel
                                videoTrackID:(NSString*)videoTrackID
                                audioTrackID:(NSString*)audioTrackID {
  RTCMediaStream* localMediaStream = [factory mediaStreamWithLabel:streamLabel];
  RTCVideoTrack* videoTrack =
      [factory videoTrackWithID:videoTrackID source:videoSource];
  RTCFakeRenderer* videoRenderer = [[RTCFakeRenderer alloc] init];
  [videoTrack addRenderer:videoRenderer];
  [localMediaStream addVideoTrack:videoTrack];
  // Test that removal/re-add works.
  [localMediaStream removeVideoTrack:videoTrack];
  [localMediaStream addVideoTrack:videoTrack];
  RTCAudioTrack* audioTrack = [factory audioTrackWithID:audioTrackID];
  [localMediaStream addAudioTrack:audioTrack];
  [pc addStream:localMediaStream];
  return localMediaStream;
}

- (void)testCompleteSessionWithFactory:(RTCPeerConnectionFactory*)factory {
  NSArray* mandatory = @[
    [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"],
    [[RTCPair alloc] initWithKey:@"internalSctpDataChannels" value:@"true"],
  ];
  RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] init];
  RTCMediaConstraints* pcConstraints =
      [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory
                                            optionalConstraints:nil];

  RTCPeerConnectionSyncObserver* offeringExpectations =
      [[RTCPeerConnectionSyncObserver alloc] init];
  RTCPeerConnection* pcOffer =
      [factory peerConnectionWithICEServers:nil
                                constraints:pcConstraints
                                   delegate:offeringExpectations];

  RTCPeerConnectionSyncObserver* answeringExpectations =
      [[RTCPeerConnectionSyncObserver alloc] init];

  RTCPeerConnection* pcAnswer =
      [factory peerConnectionWithICEServers:nil
                                constraints:pcConstraints
                                   delegate:answeringExpectations];
  // TODO(hughv): Create video capturer
  RTCVideoCapturer* capturer = nil;
  RTCVideoSource* videoSource =
      [factory videoSourceWithCapturer:capturer constraints:constraints];

  // Here and below, "oLMS" refers to offerer's local media stream, and "aLMS"
  // refers to the answerer's local media stream, with suffixes of "a0" and "v0"
  // for audio and video tracks, resp.  These mirror chrome historical naming.
  RTCMediaStream* oLMSUnused = [self addTracksToPeerConnection:pcOffer
                                                   withFactory:factory
                                                   videoSource:videoSource
                                                   streamLabel:@"oLMS"
                                                  videoTrackID:@"oLMSv0"
                                                  audioTrackID:@"oLMSa0"];

  RTCDataChannel* offerDC =
      [pcOffer createDataChannelWithLabel:@"offerDC"
                                   config:[[RTCDataChannelInit alloc] init]];
  EXPECT_TRUE([offerDC.label isEqual:@"offerDC"]);
  offerDC.delegate = offeringExpectations;
  offeringExpectations.dataChannel = offerDC;

  RTCSessionDescriptionSyncObserver* sdpObserver =
      [[RTCSessionDescriptionSyncObserver alloc] init];
  [pcOffer createOfferWithDelegate:sdpObserver constraints:constraints];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.success);
  RTCSessionDescription* offerSDP = sdpObserver.sessionDescription;
  EXPECT_EQ([@"offer" compare:offerSDP.type options:NSCaseInsensitiveSearch],
            NSOrderedSame);
  EXPECT_GT([offerSDP.description length], 0);

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [answeringExpectations expectSignalingChange:RTCSignalingHaveRemoteOffer];
  [answeringExpectations expectAddStream:@"oLMS"];
  [pcAnswer setRemoteDescriptionWithDelegate:sdpObserver
                          sessionDescription:offerSDP];
  [sdpObserver wait];

  RTCMediaStream* aLMSUnused = [self addTracksToPeerConnection:pcAnswer
                                                   withFactory:factory
                                                   videoSource:videoSource
                                                   streamLabel:@"aLMS"
                                                  videoTrackID:@"aLMSv0"
                                                  audioTrackID:@"aLMSa0"];

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [pcAnswer createAnswerWithDelegate:sdpObserver constraints:constraints];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.success);
  RTCSessionDescription* answerSDP = sdpObserver.sessionDescription;
  EXPECT_EQ([@"answer" compare:answerSDP.type options:NSCaseInsensitiveSearch],
            NSOrderedSame);
  EXPECT_GT([answerSDP.description length], 0);

  [offeringExpectations expectICECandidates:2];
  [answeringExpectations expectICECandidates:2];

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [answeringExpectations expectSignalingChange:RTCSignalingStable];
  [pcAnswer setLocalDescriptionWithDelegate:sdpObserver
                         sessionDescription:answerSDP];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.sessionDescription == NULL);

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [offeringExpectations expectSignalingChange:RTCSignalingHaveLocalOffer];
  [pcOffer setLocalDescriptionWithDelegate:sdpObserver
                        sessionDescription:offerSDP];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.sessionDescription == NULL);

  [offeringExpectations expectICEConnectionChange:RTCICEConnectionChecking];
  [offeringExpectations expectICEConnectionChange:RTCICEConnectionConnected];
  // TODO(fischman): figure out why this is flaky and re-introduce (and remove
  // special-casing from the observer!).
  // [offeringExpectations expectICEConnectionChange:RTCICEConnectionCompleted];
  [answeringExpectations expectICEConnectionChange:RTCICEConnectionChecking];
  [answeringExpectations expectICEConnectionChange:RTCICEConnectionConnected];

  [offeringExpectations expectStateChange:kRTCDataChannelStateOpen];
  [answeringExpectations expectDataChannel:@"offerDC"];
  [answeringExpectations expectStateChange:kRTCDataChannelStateOpen];

  [offeringExpectations expectICEGatheringChange:RTCICEGatheringComplete];
  [answeringExpectations expectICEGatheringChange:RTCICEGatheringComplete];

  sdpObserver = [[RTCSessionDescriptionSyncObserver alloc] init];
  [offeringExpectations expectSignalingChange:RTCSignalingStable];
  [offeringExpectations expectAddStream:@"aLMS"];
  [pcOffer setRemoteDescriptionWithDelegate:sdpObserver
                         sessionDescription:answerSDP];
  [sdpObserver wait];
  EXPECT_TRUE(sdpObserver.sessionDescription == NULL);

  EXPECT_TRUE([offerSDP.type isEqual:pcOffer.localDescription.type]);
  EXPECT_TRUE([answerSDP.type isEqual:pcOffer.remoteDescription.type]);
  EXPECT_TRUE([offerSDP.type isEqual:pcAnswer.remoteDescription.type]);
  EXPECT_TRUE([answerSDP.type isEqual:pcAnswer.localDescription.type]);

  for (RTCICECandidate* candidate in offeringExpectations
           .releaseReceivedICECandidates) {
    [pcAnswer addICECandidate:candidate];
  }
  for (RTCICECandidate* candidate in answeringExpectations
           .releaseReceivedICECandidates) {
    [pcOffer addICECandidate:candidate];
  }

  [offeringExpectations waitForAllExpectationsToBeSatisfied];
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  EXPECT_EQ(pcOffer.signalingState, RTCSignalingStable);
  EXPECT_EQ(pcAnswer.signalingState, RTCSignalingStable);

  // Test send and receive UTF-8 text
  NSString* text = @"你好";
  NSData* textData = [text dataUsingEncoding:NSUTF8StringEncoding];
  RTCDataBuffer* buffer =
      [[RTCDataBuffer alloc] initWithData:textData isBinary:NO];
  [answeringExpectations expectMessage:[textData copy] isBinary:NO];
  EXPECT_TRUE([offeringExpectations.dataChannel sendData:buffer]);
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  // Test send and receive binary data
  const size_t byteLength = 5;
  char bytes[byteLength] = {1, 2, 3, 4, 5};
  NSData* byteData = [NSData dataWithBytes:bytes length:byteLength];
  buffer = [[RTCDataBuffer alloc] initWithData:byteData isBinary:YES];
  [answeringExpectations expectMessage:[byteData copy] isBinary:YES];
  EXPECT_TRUE([offeringExpectations.dataChannel sendData:buffer]);
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  [offeringExpectations expectStateChange:kRTCDataChannelStateClosing];
  [answeringExpectations expectStateChange:kRTCDataChannelStateClosing];
  [offeringExpectations expectStateChange:kRTCDataChannelStateClosed];
  [answeringExpectations expectStateChange:kRTCDataChannelStateClosed];

  [answeringExpectations.dataChannel close];
  [offeringExpectations.dataChannel close];

  [offeringExpectations waitForAllExpectationsToBeSatisfied];
  [answeringExpectations waitForAllExpectationsToBeSatisfied];
  // Don't need to listen to further state changes.
  // TODO(tkchin): figure out why Closed->Closing without this.
  offeringExpectations.dataChannel.delegate = nil;
  answeringExpectations.dataChannel.delegate = nil;

  // Let the audio feedback run for 2s to allow human testing and to ensure
  // things stabilize.  TODO(fischman): replace seconds with # of video frames,
  // when we have video flowing.
  [[NSRunLoop currentRunLoop]
      runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];

  [offeringExpectations expectICEConnectionChange:RTCICEConnectionClosed];
  [answeringExpectations expectICEConnectionChange:RTCICEConnectionClosed];
  [offeringExpectations expectSignalingChange:RTCSignalingClosed];
  [answeringExpectations expectSignalingChange:RTCSignalingClosed];

  [pcOffer close];
  [pcAnswer close];

  [offeringExpectations waitForAllExpectationsToBeSatisfied];
  [answeringExpectations waitForAllExpectationsToBeSatisfied];

  capturer = nil;
  videoSource = nil;
  pcOffer = nil;
  pcAnswer = nil;
  // TODO(fischman): be stricter about shutdown checks; ensure thread
  // counts return to where they were before the test kicked off, and
  // that all objects have in fact shut down.
}

@end

// TODO(fischman): move {Initialize,Cleanup}SSL into alloc/dealloc of
// RTCPeerConnectionTest and avoid the appearance of RTCPeerConnectionTest being
// a TestBase since it's not.
TEST(RTCPeerConnectionTest, SessionTest) {
  @autoreleasepool {
    rtc::InitializeSSL();
    // Since |factory| will own the signaling & worker threads, it's important
    // that it outlive the created PeerConnections since they self-delete on the
    // signaling thread, and if |factory| is freed first then a last refcount on
    // the factory will expire during this teardown, causing the signaling
    // thread to try to Join() with itself.  This is a hack to ensure that the
    // factory outlives RTCPeerConnection:dealloc.
    // See https://code.google.com/p/webrtc/issues/detail?id=3100.
    RTCPeerConnectionFactory* factory = [[RTCPeerConnectionFactory alloc] init];
    @autoreleasepool {
      RTCPeerConnectionTest* pcTest = [[RTCPeerConnectionTest alloc] init];
      [pcTest testCompleteSessionWithFactory:factory];
    }
    rtc::CleanupSSL();
  }
}