25 |
26 |
27 |
31 |
32 |
33 |
34 |
Demo details
35 |
This Video Call demo is basically an example of how you can achieve a
36 | basic WebRTC video call.
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 |
Local Stream
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Remote Stream
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/Chatchat/ChatViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ChatViewController.m
3 | // Chatchat
4 | //
5 | // Created by WangRui on 16/6/1.
6 | // Copyright © 2016年 Beta.Inc. All rights reserved.
7 | //
8 |
9 | #import "ChatViewController.h"
10 | #import "UserManager.h"
11 | #import "ChatSessionManager.h"
12 |
13 | @interface ChatViewController ()
14 | {
15 | ChatSession *_session;
16 | UserManager *_userManager;
17 | ChatSessionManager *_sessionManager;
18 | }
19 |
20 | @property (strong) IBOutlet UITableView *tableView;
21 | @property (strong) IBOutlet UITextField *textField;
22 | @property (strong) IBOutlet UIScrollView *scrollView;
23 |
24 | @end
25 |
26 | @implementation ChatViewController
27 |
28 | - (void)viewDidLoad {
29 | [super viewDidLoad];
30 | // Do any additional setup after loading the view.
31 |
32 | _userManager = [UserManager sharedManager];
33 | _sessionManager = [ChatSessionManager sharedManager];
34 |
35 | _session = [_sessionManager createSessionWithPeer:self.peer];
36 | }
37 |
38 | - (void)didReceiveMemoryWarning {
39 | [super didReceiveMemoryWarning];
40 | // Dispose of any resources that can be recreated.
41 | }
42 |
43 | - (void)viewDidAppear:(BOOL)animated{
44 | [super viewDidAppear:animated];
45 |
46 | UIEdgeInsets contentInsets = UIEdgeInsetsZero;
47 | self.scrollView.contentInset = contentInsets;
48 | self.scrollView.scrollIndicatorInsets = contentInsets;
49 |
50 | [[NSNotificationCenter defaultCenter] addObserver:self
51 | selector:@selector(keyboardWasShown:)
52 | name:UIKeyboardDidShowNotification
53 | object:nil];
54 |
55 | [[NSNotificationCenter defaultCenter] addObserver:self
56 | selector:@selector(keyboardWillBeHidden:)
57 | name:UIKeyboardWillHideNotification
58 | object:nil];
59 | }
60 |
61 | - (void)viewDidDisappear:(BOOL)animated{
62 | [super viewDidDisappear:animated];
63 |
64 | [_session clearUnread];
65 |
66 | [[NSNotificationCenter defaultCenter] removeObserver:self
67 | name:UIKeyboardDidShowNotification
68 | object:nil];
69 | [[NSNotificationCenter defaultCenter] removeObserver:self
70 | name:UIKeyboardWillHideNotification
71 | object:nil];
72 | }
73 |
74 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
75 | return [[_session listAllMessages] count];
76 | }
77 |
78 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
79 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ChatTableViewCell" forIndexPath:indexPath];
80 | Message *message = [[_session listAllMessages] objectAtIndex:indexPath.row];
81 | if ([message.from isEqualToString: [_userManager localUser].uniqueID]) {
82 | cell.textLabel.text = @"";
83 | cell.detailTextLabel.text = message.content;
84 | }else{
85 | cell.textLabel.text = message.content;
86 | cell.detailTextLabel.text = nil;
87 | }
88 | return cell;
89 | }
90 |
91 | - (void)onMessage: (Message *)message{
92 | [_session onMessage:message];
93 | dispatch_async(dispatch_get_main_queue(), ^{
94 | [self.tableView reloadData];
95 | });
96 | }
97 |
98 | // Called when the UIKeyboardDidShowNotification is sent.
99 | - (void)keyboardWasShown:(NSNotification*)aNotification
100 | {
101 | NSDictionary* info = [aNotification userInfo];
102 | CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
103 |
104 | UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0);
105 | self.scrollView.contentInset = contentInsets;
106 | self.scrollView.scrollIndicatorInsets = contentInsets;
107 |
108 | // If active text field is hidden by keyboard, scroll it so it's visible
109 | // Your app might not need or want this behavior.
110 | CGRect aRect = self.view.frame;
111 | aRect.size.height -= kbSize.height;
112 | if (!CGRectContainsPoint(aRect, self.textField.frame.origin) ) {
113 | [self.scrollView scrollRectToVisible:self.textField.frame animated:YES];
114 | }
115 | }
116 |
117 | // Called when the UIKeyboardWillHideNotification is sent
118 | - (void)keyboardWillBeHidden:(NSNotification*)aNotification
119 | {
120 | UIEdgeInsets contentInsets = UIEdgeInsetsZero;
121 | self.scrollView.contentInset = contentInsets;
122 | self.scrollView.scrollIndicatorInsets = contentInsets;
123 | }
124 |
125 |
126 | - (BOOL)textFieldShouldReturn:(UITextField *)textField{
127 | [textField resignFirstResponder];
128 | return YES;
129 | }
130 |
131 | - (void)textFieldDidEndEditing:(UITextField *)textField{
132 | Message *message = [Message textMessageWithPeerUID:self.peer.uniqueID
133 | content:textField.text];
134 | [self.socketIODelegate sendMessage:message];
135 |
136 | //'received' a local message
137 | [self onMessage:message];
138 | textField.text = nil;
139 | }
140 |
141 |
142 | /*
143 | #pragma mark - Navigation
144 |
145 | // In a storyboard-based application, you will often want to do a little preparation before navigation
146 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
147 | // Get the new view controller using [segue destinationViewController].
148 | // Pass the selected object to the new view controller.
149 | }
150 | */
151 |
152 | @end
153 |
--------------------------------------------------------------------------------
/Chatchat/VideoCallViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // VideoCallViewController.m
3 | // Chatchat
4 | //
5 | // Created by WangRui on 16/6/24.
6 | // Copyright © 2016年 Beta.Inc. All rights reserved.
7 | //
8 |
9 | #import "VideoCallViewController.h"
10 |
11 | #import
12 |
13 | @interface VideoCallViewController ()
14 | {
15 | RTCVideoTrack *_localVideoTrack;
16 | RTCVideoTrack *_remoteVideoTrack;
17 |
18 | IBOutlet UILabel *_callingTitle;
19 | IBOutlet UIButton *_hangupButton;
20 |
21 | RTCEAGLVideoView *_cameraPreviewView;
22 | }
23 | @end
24 |
25 |
26 | @implementation VideoCallViewController
27 |
28 | - (RTCMediaConstraints *)defaultOfferConstraints {
29 | NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true",
30 | @"OfferToReceiveVideo": @"true"};
31 | NSDictionary *optionalConstraints = @{@"DtlsSrtpKeyAgreement" : @"false"};
32 |
33 | RTCMediaConstraints* constraints =
34 | [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints
35 | optionalConstraints:optionalConstraints];
36 | return constraints;
37 | }
38 |
39 | - (void)viewDidLoad{
40 | [super viewDidLoad];
41 |
42 | _callingTitle.text = [NSString stringWithFormat:@"Calling %@", self.peer.name];
43 | _cameraPreviewView = nil;
44 |
45 | [self startLocalStream];
46 | }
47 |
48 | - (void)startLocalStream{
49 | RTCMediaStream *localStream = [self.factory mediaStreamWithStreamId:@"localStream"];
50 | RTCAudioTrack *audioTrack = [self.factory audioTrackWithTrackId:@"audio0"];
51 | [localStream addAudioTrack : audioTrack];
52 |
53 | //How to initialize souce??
54 | RTCVideoSource *source = [self.factory avFoundationVideoSourceWithConstraints:[self defaultVideoConstraints]];
55 | RTCVideoTrack *localVideoTrack = [self.factory videoTrackWithSource:source trackId:@"video0"];
56 | [localStream addVideoTrack:localVideoTrack];
57 | _localVideoTrack = localVideoTrack;
58 |
59 | [self.peerConnection addStream:localStream];
60 |
61 | [self.peerConnection offerForConstraints:[self defaultOfferConstraints]
62 | completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
63 | [self didCreateSessionDescription:sdp error:error];
64 | }];
65 |
66 | [self startPreview];
67 | }
68 |
69 | - (void)startPreview{
70 | if (_cameraPreviewView.superview == self.view) {
71 | return;
72 | }
73 |
74 | dispatch_async(dispatch_get_main_queue(), ^{
75 | _cameraPreviewView = [[RTCEAGLVideoView alloc] initWithFrame: self.view.bounds];
76 | _cameraPreviewView.delegate = self;
77 |
78 | [self.view addSubview:_cameraPreviewView];
79 | [_localVideoTrack addRenderer:_cameraPreviewView];
80 |
81 | [self.view bringSubviewToFront:_callingTitle];
82 | [self.view bringSubviewToFront:_hangupButton];
83 |
84 | });
85 | }
86 |
87 |
88 | #pragma mark -- RTCEAGLVideoViewDelegate --
89 | - (void)videoView:(RTCEAGLVideoView*)videoView didChangeVideoSize:(CGSize)size{
90 | NSLog(@"Video size changed to: %d, %d", (int)size.width, (int)size.height);
91 | }
92 |
93 |
94 | #pragma mark -- peerConnection delegate override --
95 |
96 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
97 | didAddStream:(RTCMediaStream *)stream{
98 | [super peerConnection:peerConnection didAddStream:stream];
99 |
100 | NSLog(@"%s, video tracks: %lu", __FILE__, (unsigned long)stream.videoTracks.count);
101 |
102 | if (stream.videoTracks.count) {
103 | _remoteVideoTrack = [stream.videoTracks lastObject];
104 |
105 | dispatch_async(dispatch_get_main_queue(), ^{
106 | // Scale local view to upright corner
107 | if (_cameraPreviewView) {
108 | [UIView animateWithDuration:0.5 animations:^{
109 | NSUInteger width = 100;
110 | float screenRatio = [[UIScreen mainScreen] bounds].size.height / [[UIScreen mainScreen] bounds].size.width;
111 | NSUInteger height = width * screenRatio;
112 | _cameraPreviewView.frame = CGRectMake(self.view.bounds.size.width - 100, 0, width, height);
113 | } completion:^(BOOL finished) {
114 | // Create a new render view with a size of your choice
115 | RTCEAGLVideoView *renderView = [[RTCEAGLVideoView alloc] initWithFrame:self.view.bounds];
116 | renderView.delegate = self;
117 | [_remoteVideoTrack addRenderer:renderView];
118 | [self.view addSubview:renderView];
119 |
120 | if (_cameraPreviewView) {
121 | [self.view bringSubviewToFront:_cameraPreviewView];
122 | }
123 | [self.view bringSubviewToFront:_callingTitle];
124 | [self.view bringSubviewToFront:_hangupButton];
125 |
126 | }];
127 | }
128 | });
129 | }
130 | }
131 |
132 |
133 | - (IBAction)hangupButtonPressed:(id)sender{
134 | [self sendCloseSignal];
135 |
136 | if (self.peerConnection) {
137 | [self.peerConnection close];
138 | }
139 |
140 | [self dismissViewControllerAnimated:YES completion:nil];
141 | }
142 |
143 | - (void)sendCloseSignal{
144 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID
145 | Type:@"signal"
146 | SubType:@"close"
147 | Content:@"call is denied"];
148 | [self.socketIODelegate sendMessage:message];
149 | }
150 |
151 | @end
152 |
--------------------------------------------------------------------------------
/Chatchat/IncomingCallViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // IncomingCallViewController.m
3 | // Chatchat
4 | //
5 | // Created by WangRui on 16/6/8.
6 | // Copyright © 2016年 Beta.Inc. All rights reserved.
7 | //
8 | #import
9 |
10 | #import "IncomingCallViewController.h"
11 | #import "RTCICECandidate+JSON.h"
12 | #import "RTCSessionDescription+JSON.h"
13 |
14 | @interface IncomingCallViewController ()
15 | {
16 | RTCVideoTrack *_localVideoTrack;
17 | RTCVideoTrack *_remoteVideoTrack;
18 |
19 | BOOL _accepted;
20 |
21 | BOOL _videoEnabled;
22 | RTCEAGLVideoView *_cameraPreviewView;
23 |
24 | }
25 |
26 | @property (strong) IBOutlet UIButton *acceptButton;
27 | @property (strong) IBOutlet UIButton *denyButton;
28 | @property (strong) IBOutlet UILabel *callTitle;
29 | @end
30 |
31 | @implementation IncomingCallViewController
32 |
33 | - (RTCMediaConstraints *)defaultAnswerConstraints{
34 | NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true",
35 | @"OfferToReceiveVideo": @"true"};
36 | RTCMediaConstraints* constraints =
37 | [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints
38 | optionalConstraints:nil];
39 | return constraints;
40 | }
41 |
42 | - (RTCMediaConstraints *)defaultVideoAnswerConstraints{
43 | NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true",
44 | @"OfferToReceiveVideo": @"true"};
45 | RTCMediaConstraints* constraints =
46 | [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints
47 | optionalConstraints:nil];
48 | return constraints;
49 | }
50 |
51 | - (void)viewDidLoad{
52 | [super viewDidLoad];
53 |
54 | _videoEnabled = NO;
55 | _cameraPreviewView = nil;
56 | _accepted = NO;
57 |
58 | _localVideoTrack = nil;
59 | _remoteVideoTrack = nil;
60 |
61 | NSString *title = [NSString stringWithFormat:@"Call From %@", self.peer.name];
62 | NSString *sdp = [self.offer.content objectForKey:@"sdp"];
63 | if ([sdp containsString:@"video"]) {
64 | _videoEnabled = YES;
65 | title = [NSString stringWithFormat:@"Video Call From %@", self.peer.name];
66 | }
67 |
68 | self.callTitle.text = title;
69 |
70 | __weak IncomingCallViewController *weakSelf = self;
71 | RTCSessionDescription *offer = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdp];
72 | [self.peerConnection setRemoteDescription:offer
73 | completionHandler:^(NSError * _Nullable error) {
74 | [weakSelf didSetSessionDescriptionWithError:error];
75 | }];
76 |
77 | RTCMediaStream *localStream = [self.factory mediaStreamWithStreamId:@"localStream"];
78 | RTCAudioTrack *audioTrack = [self.factory audioTrackWithTrackId:@"audio0"];
79 | [localStream addAudioTrack : audioTrack];
80 |
81 | if (_videoEnabled) {
82 | RTCVideoSource *source = [self.factory avFoundationVideoSourceWithConstraints:[self defaultVideoConstraints]];
83 | RTCVideoTrack *localVideoTrack = [self.factory videoTrackWithSource:source trackId:@"video0"];
84 | [localStream addVideoTrack:localVideoTrack];
85 |
86 | _localVideoTrack = localVideoTrack;
87 |
88 | [self startPreview];
89 | }
90 |
91 | [self.peerConnection addStream:localStream];
92 |
93 | NSLog(@"%s, presenting view with offer: %@", __FILE__, self.offer.content);
94 | }
95 |
96 | - (void)viewDidAppear:(BOOL)animated{
97 | [super viewDidAppear:animated];
98 |
99 | //deal with any pending signal message after view loaded
100 | for (Message *item in self.pendingMessages) {
101 | [self onMessage:item];
102 | }
103 | }
104 |
105 | - (void)startPreview{
106 | if (_cameraPreviewView.superview == self.view) {
107 | return;
108 | }
109 | dispatch_async(dispatch_get_main_queue(), ^{
110 | NSUInteger width = 100;
111 | float screenRatio = [[UIScreen mainScreen] bounds].size.height / [[UIScreen mainScreen] bounds].size.width;
112 | NSUInteger height = width * screenRatio;
113 | _cameraPreviewView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(self.view.bounds.size.width - width, 0, width, height)];
114 | _cameraPreviewView.delegate = self;
115 | [_localVideoTrack addRenderer:_cameraPreviewView];
116 |
117 | [self.view addSubview:_cameraPreviewView];
118 | [self.view bringSubviewToFront:self.callTitle];
119 | [self.view bringSubviewToFront:self.acceptButton];
120 |
121 | });
122 | }
123 |
124 | - (void)startRemoteVideo{
125 | dispatch_async(dispatch_get_main_queue(), ^{
126 | RTCEAGLVideoView *renderView = [[RTCEAGLVideoView alloc] initWithFrame:self.view.bounds];
127 | renderView.delegate = self;
128 | [_remoteVideoTrack addRenderer:renderView];
129 | [self.view addSubview:renderView];
130 |
131 | if (_cameraPreviewView) {
132 | [self.view bringSubviewToFront:_cameraPreviewView];
133 | }
134 | [self.view bringSubviewToFront:self.acceptButton];
135 | [self.view bringSubviewToFront:self.callTitle];
136 |
137 | });
138 | }
139 |
140 | #pragma mark -- RTCEAGLVideoViewDelegate --
141 | - (void)videoView:(RTCEAGLVideoView*)videoView didChangeVideoSize:(CGSize)size{
142 | NSLog(@"Video size changed to: %d, %d", (int)size.width, (int)size.height);
143 | }
144 |
145 | #pragma mark -- peerConnection delegate override --
146 |
147 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
148 | didAddStream:(RTCMediaStream *)stream{
149 | // Create a new render view with a size of your choice
150 | [super peerConnection:peerConnection didAddStream:stream];
151 |
152 | if (stream.videoTracks.count) {
153 | _remoteVideoTrack = [stream.videoTracks lastObject];
154 | }
155 | }
156 |
157 | - (void)onMessage: (Message *)message{
158 | if ([message.type isEqualToString:@"signal"]) {
159 | if ([message.subtype isEqualToString:@"offer"]) {
160 | //
161 | //create peerconnection
162 | //set remote desc
163 | //I'm calling in, so I don't accept offer right now
164 |
165 | }else if([message.subtype isEqualToString:@"answer"]){
166 |
167 | }else if([message.subtype isEqualToString:@"candidate"]){
168 | NSLog(@"%s, got candidate from peer: %@", __FILE__, message.content);
169 | RTCIceCandidate *candidate = [RTCIceCandidate candidateFromJSONDictionary:message.content];
170 | [self.peerConnection addIceCandidate:candidate];
171 |
172 | }else if([message.subtype isEqualToString:@"close"]){
173 | [self handleRemoteHangup];
174 | }
175 | }
176 | }
177 |
178 | - (void)handleRemoteHangup{
179 | [self.peerConnection close];
180 |
181 | //TODO play busy tone
182 | [self dismissViewControllerAnimated:YES completion:nil];
183 | }
184 |
185 |
186 | - (IBAction)acceptButtonPressed:(id)sender{
187 | if (_accepted) {
188 | //close pc
189 | //dismiss this vc
190 | [self sendCloseSignal];
191 | [self.peerConnection close];
192 |
193 | [self dismissViewControllerAnimated:YES completion:nil];
194 |
195 | }else{
196 | NSLog(@"call accepted");
197 |
198 | RTCMediaConstraints *constraints = [self defaultAnswerConstraints];
199 | if (_videoEnabled) {
200 | constraints = [self defaultVideoAnswerConstraints];
201 | }
202 | [self.peerConnection answerForConstraints:constraints
203 | completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
204 | [self didCreateSessionDescription:sdp error:error];
205 | }];
206 |
207 | _accepted = YES;
208 |
209 | self.denyButton.hidden = YES;
210 |
211 | [self.acceptButton setTitle:@"Hangup" forState:UIControlStateNormal];
212 |
213 | if (_videoEnabled) {
214 | dispatch_async(dispatch_get_main_queue(), ^{
215 | [self startPreview];
216 | [self startRemoteVideo];
217 | });
218 | }
219 | }
220 | }
221 |
222 | - (IBAction)denyButtonPressed:(id)sender{
223 | //signal denied
224 | //close pc
225 | //dismiss this vc
226 | NSLog(@"call denied");
227 |
228 | [self sendCloseSignal];
229 | [self.peerConnection close];
230 | [self dismissViewControllerAnimated:YES completion:nil];
231 | }
232 |
233 | - (void)sendCloseSignal{
234 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID
235 | Type:@"signal"
236 | SubType:@"close"
237 | Content:@"call is denied"];
238 | [self.socketIODelegate sendMessage:message];
239 | }
240 |
241 | @end
242 |
243 |
--------------------------------------------------------------------------------
/Chatchat/CallViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // CallViewController.m
3 | // Chatchat
4 | //
5 | // Created by WangRui on 16/7/11.
6 | // Copyright © 2016年 Beta.Inc. All rights reserved.
7 | //
8 |
9 | #import "CallViewController.h"
10 |
11 | #import "RTCICECandidate+JSON.h"
12 |
13 | #import "ARDSDPUtils.h"
14 |
15 | #import "RTCSessionDescription+JSON.h"
16 |
17 | //enum to string.
18 | //Modify these names when defination changed
19 | NSString *const RTCIceStateNames[] = {
20 | @"RTCICEConnectionNew",
21 | @"RTCICEConnectionChecking",
22 | @"RTCICEConnectionConnected",
23 | @"RTCICEConnectionCompleted",
24 | @"RTCICEConnectionFailed",
25 | @"RTCICEConnectionDisconnected",
26 | @"RTCICEConnectionClosed",
27 | @"RTCICEConnectionMax"
28 | };
29 |
30 | @interface CallViewController ()
31 | {
32 | RTCFileLogger *_logger;
33 | NSTimer *_statTimer;
34 | }
35 |
36 | @end
37 |
38 | @implementation CallViewController
39 |
40 | - (RTCMediaConstraints *)defaultMediaConstraints{
41 | return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:nil];
42 | }
43 |
44 | - (RTCMediaConstraints *)defaultVideoConstraints{
45 | float screenRatio = [[UIScreen mainScreen] bounds].size.height / [[UIScreen mainScreen] bounds].size.width;
46 | NSDictionary *mandatoryConstraints = @{@"minAspectRatio" : [NSString stringWithFormat:@"%.1f", screenRatio - 0.1],
47 | @"maxAspectRatio" : [NSString stringWithFormat:@"%.1f", screenRatio + 0.1]};
48 | return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:nil];
49 | }
50 |
51 | - (NSArray *)defaultIceServers{
52 | return @[[[RTCIceServer alloc] initWithURLStrings:@[kDefaultSTUNServerUrl] username:@"" credential:@""]];
53 | }
54 |
55 | - (RTCMediaConstraints *)defaultPeerConnectionConstraints {
56 | NSDictionary *optionalConstraints = @{@"DtlsSrtpKeyAgreement" : @"true"};
57 | RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil
58 | optionalConstraints:optionalConstraints];
59 | return constraints;
60 | }
61 |
62 |
63 | - (void)viewDidLoad {
64 | [super viewDidLoad];
65 | // Do any additional setup after loading the view.
66 |
67 | _logger = [[RTCFileLogger alloc] init];
68 | [_logger start];
69 |
70 | RTCInitializeSSL();
71 |
72 | _factory = [[RTCPeerConnectionFactory alloc] init];
73 |
74 | RTCConfiguration *configure = [[RTCConfiguration alloc] init];
75 | configure.iceServers = [self defaultIceServers];
76 |
77 | _peerConnection = [_factory peerConnectionWithConfiguration:configure
78 | constraints:[self defaultMediaConstraints]
79 | delegate:self];
80 |
81 | }
82 |
83 | - (void)didReceiveMemoryWarning {
84 | [super didReceiveMemoryWarning];
85 | // Dispose of any resources that can be recreated.
86 | }
87 |
88 | - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion{
89 | [super dismissViewControllerAnimated:flag completion:completion];
90 |
91 | if (_statTimer) {
92 | [_statTimer invalidate];
93 | }
94 |
95 | RTCCleanupSSL();
96 | }
97 |
98 | #pragma mark -- RTCSessionDescriptionDelegate --
99 | - (void)didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error
100 | {
101 | NSLog(@"didCreateSessionDescription : %ld:%@", (long)sdp.type, sdp.description);
102 | RTCSessionDescription *descriptionPreferH264 = [ARDSDPUtils descriptionForDescription:sdp preferredVideoCodec:@"H264"];
103 |
104 | __weak CallViewController *weakSelf = self;
105 | [self.peerConnection setLocalDescription:descriptionPreferH264
106 | completionHandler:^(NSError * _Nullable error) {
107 | [weakSelf didSetSessionDescriptionWithError:error];
108 | }];
109 |
110 | NSString *subtype = (sdp.type == RTCSdpTypeOffer) ? @"offer" : @"answer";
111 | // Send offer through the signaling channel of our application
112 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID
113 | Type:@"signal"
114 | SubType:subtype
115 | Content:[descriptionPreferH264 toDictionary]];
116 |
117 | [self.socketIODelegate sendMessage:message];
118 | }
119 |
120 | - (void)didSetSessionDescriptionWithError:(NSError *)error
121 | {
122 | if (_peerConnection.signalingState == RTCSignalingStateHaveLocalOffer) {
123 | NSLog(@"have local offer");
124 | }else if (_peerConnection.signalingState == RTCSignalingStateHaveRemoteOffer){
125 | NSLog(@"have remote offer");
126 | }else if(_peerConnection.signalingState == RTCSignalingStateHaveRemotePrAnswer){
127 | NSLog(@"have remote answer");
128 | }
129 | }
130 |
131 |
132 | #pragma mark -- peerConnection delegate --
133 |
134 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
135 | didAddStream:(nonnull RTCMediaStream *)stream{
136 | NSLog(@"%s is called", __FUNCTION__);
137 | NSLog(@"audio tracks: %lu", (unsigned long)stream.audioTracks.count);
138 | // Create a new render view with a size of your choice
139 | // RTCEAGLVideoView *renderView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
140 | // [stream.videoTracks.lastObject addRenderer:renderView];
141 | }
142 |
143 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
144 | didChangeSignalingState:(RTCSignalingState)stateChanged{
145 | NSLog(@"signaling state changed: %ld", (long)stateChanged);
146 | }
147 |
148 | // Triggered when a remote peer close a stream.
149 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
150 | didRemoveStream:(nonnull RTCMediaStream *)stream{
151 | NSLog(@"removed stream");
152 | }
153 |
154 | // Triggered when renegotiation is needed, for example the ICE has restarted.
155 | - (void)peerConnectionShouldNegotiate:(RTCPeerConnection *)peerConnection{
156 |
157 | }
158 |
159 | // Called any time the ICEConnectionState changes.
160 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
161 | didChangeIceConnectionState:(RTCIceConnectionState)newState{
162 | NSLog(@"Ice changed: %@", RTCIceStateNames[newState]);
163 |
164 | if (newState == RTCIceConnectionStateConnected) {
165 | dispatch_async(dispatch_get_main_queue(), ^{
166 | if (!_statTimer) {
167 | _statTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(statTimerFired:) userInfo:nil repeats:YES];
168 | [_statTimer fire];
169 | }
170 | });
171 | }
172 | }
173 |
174 | // Called any time the ICEGatheringState changes.
175 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
176 | didChangeIceGatheringState:(RTCIceGatheringState)newState{
177 |
178 | }
179 |
180 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
181 | didGenerateIceCandidate:(RTCIceCandidate *)candidate{
182 | NSLog(@"got candidate: %@", candidate.sdp);
183 |
184 | dispatch_async(dispatch_get_main_queue(), ^{
185 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID
186 | Type:@"signal"
187 | SubType:@"candidate"
188 | Content:[candidate toDictionary]];
189 | [self.socketIODelegate sendMessage:message];
190 | });
191 | }
192 |
193 | - (void)peerConnection:(RTCPeerConnection *)peerConnection
194 | didRemoveIceCandidates:(NSArray *)candidates{
195 |
196 | }
197 |
198 | // New data channel has been opened.
199 | - (void)peerConnection:(RTCPeerConnection*)peerConnection
200 | didOpenDataChannel:(RTCDataChannel*)dataChannel{
201 |
202 | }
203 |
204 |
205 | - (void)onMessage: (Message *)message{
206 | NSLog(@"onMessage:");
207 | NSLog(@"%@", message);
208 | }
209 |
210 | #pragma mark -- Stat Report --
211 | - (void)statTimerFired: (id)sender{
212 | for (RTCMediaStream *stream in _peerConnection.localStreams) {
213 | for (RTCVideoTrack *track in stream.audioTracks) {
214 | [_peerConnection statsForTrack:track statsOutputLevel:RTCStatsOutputLevelStandard completionHandler:^(NSArray * _Nonnull stats) {
215 | NSLog(@"%@", stats);
216 | }];
217 | }
218 | for (RTCVideoTrack *track in stream.videoTracks) {
219 | [_peerConnection statsForTrack:track statsOutputLevel:RTCStatsOutputLevelStandard completionHandler:^(NSArray * _Nonnull stats) {
220 | NSLog(@"%@", stats);
221 | }];
222 | }
223 | }
224 | }
225 |
226 |
227 | /*
228 | #pragma mark - Navigation
229 |
230 | // In a storyboard-based application, you will often want to do a little preparation before navigation
231 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
232 | // Get the new view controller using [segue destinationViewController].
233 | // Pass the selected object to the new view controller.
234 | }
235 | */
236 |
237 | @end
238 |
--------------------------------------------------------------------------------
/Server/public/main.js:
--------------------------------------------------------------------------------
1 |
2 | var myusername;
3 | var peerusername;
4 | var socket = io();
5 | var pc;
6 | var localStream;
7 | var remoteCandidates = [];
8 | var sendChannel;
9 | var receiveChannel;
10 |
11 | $(document).ready(function(){
12 | console.log("document ready.");
13 | console.log(adapter.browserDetails.browser);
14 |
15 | $(this).attr('disabled', true).unbind('click');
16 | // Make sure the browser supports WebRTC
17 | if(!isWebrtcSupported()) {
18 | bootbox.alert("No WebRTC support... ");
19 | return;
20 | }
21 |
22 | $('#videocall').removeClass('hide').show();
23 | $('#login').removeClass('hide').show();
24 | $('#registernow').removeClass('hide').show();
25 | $('#register').click(registerUsername);
26 | $('#username').focus();
27 | });
28 |
29 |
30 | socket.on('register succeed', function(msg){
31 | myusername = msg["name"];
32 | console.log("Successfully registered as " + myusername + "!");
33 | $('#youok').removeClass('hide').show().html("Registered as '" + myusername + "'");
34 | // Get a list of available peers, just for fun
35 | //videocall.send({"message": { "request": "list" }});
36 | // TODO Enable buttons to call now
37 | $('#phone').removeClass('hide').show();
38 | $('#call').unbind('click').click(doCall);
39 | $('#peer').focus();
40 |
41 | openDevices();
42 | });
43 |
44 | socket.on('register failed', function(msg){
45 | console.log("register failed: " + msg.info);
46 | })
47 |
48 | socket.on('new user', function(data){
49 | console.log("new user " + data.name);
50 | });
51 |
52 | socket.on('user leave', function(data){
53 | console.log(data.name + " left");
54 | });
55 |
56 | socket.on('chat message', function(data){
57 | //dispatch signal messages to corresponding functions, ie ,
58 | //onRemoteOffer/ onRemoteAnswer/onRemoteIceCandidate
59 | //IS this message to me ?
60 | if(data.to != myusername){
61 | return;
62 | }
63 | //
64 | if(data.type == 'signal'){
65 | onSignalMessage(data);
66 | }else if(data.type == 'text'){
67 | console.log('received text message from ' + data.from + ', content: ' + data.content);
68 | }else{
69 | console.log('received unknown message type ' + data.type + ' from ' + data.from);
70 | }
71 | });
72 |
73 | socket.on('connect', function(){
74 | console.log("server connected");
75 | });
76 |
77 | function isWebrtcSupported() {
78 | return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null;
79 | };
80 |
81 | function checkEnter(field, event) {
82 | var theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
83 | if(theCode == 13) {
84 | if(field.id == 'username')
85 | registerUsername();
86 | else if(field.id == 'peer')
87 | doCall();
88 | else if(field.id == 'datasend')
89 | sendData();
90 | return false;
91 | } else {
92 | return true;
93 | }
94 | }
95 |
96 | function sendData() {
97 | const data = $('#datasend').val();
98 | sendChannel.send(data);
99 | console.log('Sent Data: ' + data);
100 | $('#datasend').val('');
101 | }
102 |
103 | function registerUsername() {
104 | // Try a registration
105 | $('#username').attr('disabled', true);
106 | $('#register').attr('disabled', true).unbind('click');
107 | var username = $('#username').val();
108 | if(username === "") {
109 | bootbox.alert("Insert a username to register (e.g., pippo)");
110 | $('#username').removeAttr('disabled');
111 | $('#register').removeAttr('disabled').click(registerUsername);
112 | return;
113 | }
114 | if(/[^a-zA-Z0-9]/.test(username)) {
115 | bootbox.alert('Input is not alphanumeric');
116 | $('#username').removeAttr('disabled').val("");
117 | $('#register').removeAttr('disabled').click(registerUsername);
118 | return;
119 | }
120 | var info = { "name": username };
121 | socket.emit("register", info);
122 | console.log("trying to register as " + username);
123 | }
124 |
125 | function openDevices(){
126 | var options = {audio:false, video:true};
127 | navigator.mediaDevices
128 | .getUserMedia(options)
129 | .then(onLocalStream)
130 | .catch(function(e) {
131 | alert('getUserMedia() failed');
132 | console.log('getUserMedia() error: ', e);
133 | });
134 | }
135 |
136 | function onLocalStream(stream) {
137 | console.log('Received local stream');
138 | $('#videos').removeClass('hide').show();
139 | if($('#myvideo').length === 0)
140 | $('#videoleft').append('');
141 | $('#myvideo').get(0).srcObject = stream;
142 | $("#myvideo").get(0).muted = "muted";
143 |
144 | var videoTracks = stream.getVideoTracks();
145 | var audioTracks = stream.getAudioTracks();
146 | if (videoTracks.length > 0) {
147 | console.log('Using video device: ' + videoTracks[0].label);
148 | }
149 | if (audioTracks.length > 0) {
150 | console.log('Using audio device: ' + audioTracks[0].label);
151 | }
152 | localStream = stream;
153 | }
154 |
155 | function createPc(){
156 | var configuration = { "iceServers": [{ "urls": "stun:stun.ideasip.com" }] };
157 | pc = new RTCPeerConnection(configuration);
158 | console.log('Created local peer connection object pc');
159 |
160 | sendChannel = pc.createDataChannel("sendchannel");
161 | sendChannel.onopen = onSendChannelStateChange;
162 | sendChannel.onclose = onSendChannelStateChange;
163 |
164 | pc.onicecandidate = function(e) {
165 | onIceCandidate(pc, e);
166 | };
167 |
168 | pc.oniceconnectionstatechange = function(e) {
169 | onIceStateChange(pc, e);
170 | };
171 |
172 | pc.ondatachannel = receiveChannelCallback;
173 | pc.ontrack = gotRemoteTrack;
174 | pc.addStream(localStream);
175 | console.log('Added local stream to pc');
176 | }
177 |
178 | function doCall() {
179 | // Call someone
180 | $('#peer').attr('disabled', true);
181 | $('#call').attr('disabled', true).unbind('click');
182 | var username = $('#peer').val();
183 | if(username === "") {
184 | bootbox.alert("Insert a username to call (e.g., pluto)");
185 | $('#peer').removeAttr('disabled');
186 | $('#call').removeAttr('disabled').click(doCall);
187 | return;
188 | }
189 | if(/[^a-zA-Z0-9]/.test(username)) {
190 | bootbox.alert('Input is not alphanumeric');
191 | $('#peer').removeAttr('disabled').val("");
192 | $('#call').removeAttr('disabled').click(doCall);
193 | return;
194 | }
195 | // Call this user
196 | peerusername = username;
197 |
198 | createPc();
199 |
200 | console.log(' createOffer start');
201 | var offerOptions = {
202 | offerToReceiveAudio: 0,
203 | offerToReceiveVideo: 1,
204 | voiceActivityDetection: false
205 | };
206 |
207 | pc.createOffer(
208 | offerOptions
209 | ).then(
210 | onCreateOfferSuccess,
211 | onCreateSessionDescriptionError
212 | );
213 | }
214 |
215 | function onCreateSessionDescriptionError(error) {
216 | console.log('Failed to create session description: ' + error.toString());
217 | bootbox.alert("WebRTC error... " + JSON.stringify(error));
218 | }
219 |
220 | function onCreateOfferSuccess(desc) {
221 | console.log('Offer from pc\n' + desc.sdp);
222 | console.log('pc setLocalDescription start');
223 | pc.setLocalDescription(desc).then(
224 | function() {
225 | onSetLocalSuccess(pc);
226 | },
227 | onSetSessionDescriptionError
228 | );
229 |
230 | //Send offer to remote side
231 | var message = {from: myusername, to:peerusername, type: 'signal', subtype: 'offer', content: desc, time:new Date()};
232 | socket.emit('chat message', message);
233 |
234 | bootbox.alert("Waiting for the peer to answer...");
235 | }
236 |
237 | function onSetLocalSuccess(pc) {
238 | console.log(' setLocalDescription complete');
239 | }
240 |
241 | function onSetRemoteSuccess(pc) {
242 | console.log(' setRemoteDescription complete');
243 | applyRemoteCandidates();
244 | }
245 |
246 | function onSetSessionDescriptionError(error) {
247 | console.log('Failed to set session description: ' + error.toString());
248 | }
249 |
250 | function gotRemoteTrack(e) {
251 | if($('#remotevideo').length === 0) {
252 | addButtons = true;
253 | $('#videoright').append('');
254 | // Show the video, hide the spinner and show the resolution when we get a playing event
255 | $("#remotevideo").bind("playing", function () {
256 | if(this.videoWidth)
257 | $('#remotevideo').removeClass('hide').show();
258 | });
259 | $('#callee').removeClass('hide').html(peerusername).show();
260 | }
261 | $('#remotevideo').get(0).srcObject = e.streams[0];
262 | console.log('pc received remote track');
263 | }
264 |
265 | function onSignalMessage(m){
266 | if(m.subtype == 'offer'){
267 | console.log('got remote offer from ' + m.from + ', content ' + m.content);
268 | onSignalOffer(m);
269 | }else if(m.subtype == 'answer'){
270 | onSignalAnswer(m.content);
271 | }else if(m.subtype == 'candidate'){
272 | onSignalCandidate(m.content);
273 | }else if(m.subtype == 'close'){
274 | onSignalClose();
275 | }else{
276 | console.log('unknown signal type ' + m.subtype);
277 | }
278 | }
279 |
280 |
281 | function onSignalOffer(msg){
282 |
283 | console.log("Incoming call from " + msg["from"] + "!");
284 | peerusername = msg["from"];
285 | // Notify user
286 | bootbox.hideAll();
287 | incoming = bootbox.dialog({
288 | message: "Incoming call from " + peerusername + "!",
289 | title: "Incoming call",
290 | closeButton: false,
291 | buttons: {
292 | success: {
293 | label: "Answer",
294 | className: "btn-success",
295 | callback: function() {
296 | createPc();
297 | var Offer = msg["content"];
298 | incoming = null;
299 | $('#peer').val(peerusername).attr('disabled', true);
300 | console.log('on remoteOffer :'+ Offer.sdp);
301 | pc.setRemoteDescription(Offer).then(function(){
302 | onSetRemoteSuccess(pc)}, onSetSessionDescriptionError
303 | );
304 | pc.createAnswer().then(
305 | onCreateAnswerSuccess,
306 | onCreateSessionDescriptionError
307 | );
308 | }
309 | },
310 | danger: {
311 | label: "Decline",
312 | className: "btn-danger",
313 | callback: function() {
314 | doHangup();
315 | }
316 | }
317 | }
318 | });
319 | }
320 |
321 | function onSignalCandidate(candidate){
322 | onRemoteIceCandidate(candidate);
323 | }
324 |
325 | function onSignalAnswer(answer){
326 | bootbox.hideAll();
327 | onRemoteAnswer(answer);
328 | }
329 |
330 | function onSignalClose(){
331 | bootbox.hideAll();
332 | console.log('Call end ');
333 | pc.close();
334 | pc = null;
335 |
336 | peerusername = null;
337 | clearViews();
338 | }
339 |
340 | function onRemoteAnswer(answer){
341 | console.log('onRemoteAnswer : ' + answer);
342 | pc.setRemoteDescription(answer).then(function(){onSetRemoteSuccess(pc)}, onSetSessionDescriptionError);
343 | $('#call').removeAttr('disabled').html('Hangup')
344 | .removeClass("btn-success").addClass("btn-danger")
345 | .unbind('click').click(doHangup);
346 | }
347 |
348 |
349 | function onRemoteIceCandidate(candidate){
350 | console.log('onRemoteIceCandidate : ' + candidate);
351 | if(pc){
352 | addRemoteCandidate(candidate);
353 | }else{
354 | remoteCandidates.push(candidate);
355 | }
356 | }
357 |
358 | function applyRemoteCandidates(){
359 | for(var candidate in remoteCandidates){
360 | addRemoteCandidate(candidate);
361 | }
362 | remoteCandidates = [];
363 | }
364 |
365 | function addRemoteCandidate(candidate){
366 | pc.addIceCandidate(candidate).then(
367 | function() {
368 | onAddIceCandidateSuccess(pc);
369 | },
370 | function(err) {
371 | onAddIceCandidateError(pc, err);
372 | });
373 | }
374 |
375 |
376 | function onCreateAnswerSuccess(desc) {
377 | console.log('onCreateAnswerSuccess');
378 |
379 | pc.setLocalDescription(desc).then(
380 | function() {
381 | onSetLocalSuccess(pc);
382 | },
383 | onSetSessionDescriptionError
384 | );
385 |
386 | $('#peer').attr('disabled', true);
387 | $('#call').removeAttr('disabled').html('Hangup')
388 | .removeClass("btn-success").addClass("btn-danger")
389 | .unbind('click').click(doHangup);
390 |
391 | //Sent answer to remote side
392 | var message = {from: myusername, to:peerusername, type: 'signal', subtype: 'answer', content: desc, time:new Date()};
393 | socket.emit('chat message', message);
394 | }
395 |
396 |
397 | function onIceCandidate(pc, event) {
398 | if (event.candidate) {
399 | console.log( ' ICE candidate: \n' + event.candidate.candidate);
400 |
401 | //Send candidate to remote side
402 | var message = {from: myusername, to:peerusername, type: 'signal', subtype: 'candidate', content: event.candidate, time:new Date()};
403 | socket.emit('chat message', message);
404 | }
405 | }
406 |
407 | function onAddIceCandidateSuccess(pc) {
408 | console.log( ' addIceCandidate success');
409 | }
410 |
411 | function onAddIceCandidateError(pc, error) {
412 | console.log( ' failed to add ICE Candidate: ' + error.toString());
413 | }
414 |
415 | function onIceStateChange(pc, event) {
416 | if (pc) {
417 | console.log( ' ICE state: ' + pc.iceConnectionState);
418 | console.log('ICE state change event: ', event);
419 | }
420 | }
421 |
422 |
423 | function onSendChannelStateChange() {
424 | const readyState = sendChannel.readyState;
425 | console.log('Send channel state is: ' + readyState);
426 | if (readyState === 'open') {
427 | $('#datasend').removeAttr('disabled');
428 | } else {
429 | $('#datasend').attr('disabled', true);
430 | }
431 | }
432 |
433 | function receiveChannelCallback(event) {
434 | console.log('Receive Channel Callback');
435 | receiveChannel = event.channel;
436 | receiveChannel.onmessage = onReceiveMessageCallback;
437 | receiveChannel.onopen = onReceiveChannelStateChange;
438 | receiveChannel.onclose = onReceiveChannelStateChange;
439 | }
440 |
441 | function onReceiveMessageCallback(event) {
442 | console.log('Received Message');
443 | $('#datarecv').val(event.data);
444 | }
445 |
446 | function onReceiveChannelStateChange() {
447 | const readyState = receiveChannel.readyState;
448 | console.log(`Receive channel state is: ${readyState}`);
449 | }
450 |
451 |
452 | function doHangup() {
453 | console.log('Hangup call');
454 | sendChannel.close();
455 | receiveChannel.close();
456 |
457 | pc.close();
458 | pc = null;
459 |
460 |
461 | //Send signal to remote side
462 | var message = {from: myusername, to:peerusername, type: 'signal', subtype: 'close', content: 'close', time:new Date()};
463 | socket.emit('chat message', message);
464 |
465 | peerusername = null;
466 | clearViews();
467 | }
468 |
469 |
470 | function clearViews(){
471 | //$('#myvideo').remove();
472 | $('#remotevideo').remove();
473 | $("#videoleft").parent().unblock();
474 | $('#callee').empty().hide();
475 | peerusername = null;
476 | //$('#videos').hide();
477 |
478 | $('#call').removeAttr('disabled').unbind('click').click(doCall).html('Call').removeClass("btn-danger").addClass("btn-success");
479 | $('#peer').removeAttr('disabled').val("");
480 |
481 | $('#datasend').val('');
482 | $('#datarecv').val('');
483 | $('#datasend').attr('disabled', true);
484 | $('#datarecv').attr('disabled', true);
485 | }
486 |
487 |
--------------------------------------------------------------------------------
/Chatchat/ContactsTableViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ContactsTableViewController.m
3 | // Chatchat
4 | //
5 | // Created by WangRui on 16/5/31.
6 | // Copyright © 2016年 Beta.Inc. All rights reserved.
7 | //
8 |
9 | #import "ContactsTableViewController.h"
10 | #import "ChatViewController.h"
11 | #import "CommonDefines.h"
12 | #import "ChatSessionManager.h"
13 | #import "UserManager.h"
14 | #import "VoiceCallViewController.h"
15 | #import "VideoCallViewController.h"
16 | #import "IncomingCallViewController.h"
17 |
18 | @import SocketIO;
19 |
20 | @interface ContactsTableViewController ()
22 | {
23 | SocketIOClient *_sio;
24 | NSString *_hostAddr;
25 | UISearchController *_searchController;
26 | BOOL _serverConnected;
27 | NSTimer *_connectionTimer;
28 | UITextField *_inputTextField;
29 |
30 | ChatSessionManager *_sessionManager;
31 | UserManager *_userManager;
32 |
33 | NSMutableArray *_unReadSignalingMessages;
34 | }
35 |
36 | @property (weak) IBOutlet UILabel *footerLabel;
37 | @end
38 |
39 | @implementation ContactsTableViewController
40 |
41 | - (void)viewDidLoad {
42 | [super viewDidLoad];
43 |
44 | // Uncomment the following line to preserve selection between presentations.
45 | // self.clearsSelectionOnViewWillAppear = NO;
46 |
47 | // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
48 | // self.navigationItem.rightBarButtonItem = self.editButtonItem;
49 | _serverConnected = false;
50 | _searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
51 | _searchController.searchResultsUpdater = self;
52 | _searchController.delegate = self;
53 | self.tableView.tableHeaderView = _searchController.searchBar;
54 |
55 | _sessionManager = [ChatSessionManager sharedManager];
56 | _userManager = [UserManager sharedManager];
57 |
58 | [[NSNotificationCenter defaultCenter] addObserver:self
59 | selector:@selector(didBecomeActive:)
60 | name:UIApplicationDidBecomeActiveNotification
61 | object:nil];
62 | }
63 |
64 | - (void)didReceiveMemoryWarning {
65 | [super didReceiveMemoryWarning];
66 | // Dispose of any resources that can be recreated.
67 | }
68 |
69 | - (void)didBecomeActive : (NSNotification *)notification{
70 | if (_serverConnected) {
71 | [self.tableView reloadData];
72 | }
73 | }
74 |
75 | - (void)viewDidAppear:(BOOL)animated{
76 | [super viewDidAppear:animated];
77 |
78 | if (_serverConnected) {
79 | [self.tableView reloadData];
80 | }else{
81 |
82 | #ifdef TEST
83 | _hostAddr = @"192.168.127.241";
84 | [self setupSocketIO];
85 | #else
86 | [self showServerInputView];
87 | #endif
88 | }
89 |
90 |
91 | if (!_connectionTimer) {
92 | _connectionTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
93 | target:self
94 | selector:@selector(checkConnection)
95 | userInfo:nil
96 | repeats:YES];
97 | }
98 | }
99 |
100 | - (void)showServerInputView{
101 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Input Your Host" message:nil preferredStyle:UIAlertControllerStyleAlert];
102 | [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
103 | textField.textAlignment = NSTextAlignmentCenter;
104 | textField.clearButtonMode = UITextFieldViewModeWhileEditing;
105 | textField.placeholder = @"e.g. 192.168.1.100";
106 | [textField setKeyboardType:UIKeyboardTypeDefault];
107 | _inputTextField = textField;
108 | }];
109 | [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
110 | _hostAddr = _inputTextField.text;
111 | NSLog(@"server addr : %@", _hostAddr);
112 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
113 | [self setupSocketIO];
114 | });
115 | }]];
116 | [self presentViewController:alert animated:YES completion:nil];
117 | }
118 |
119 | - (void)checkConnection{
120 | if (!_serverConnected) {
121 | [_sio connect];
122 | }
123 | }
124 |
125 | - (void)setupSocketIO{
126 | NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:3000", _hostAddr]];
127 | _sio = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"voipEnabled" : @YES,
128 | @"log": @NO,
129 | @"forceWebsockets" : @YES,
130 | // @"secure" : @YES,
131 | @"forcePolling": @YES}];
132 | [_sio on:@"connect" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
133 | NSLog(@"connected");
134 | _serverConnected = YES;
135 | NSString *deviceName = [[UIDevice currentDevice] name];
136 | NSDictionary *dic = @{@"name" : deviceName, @"uuid" : [UIDevice currentDevice].identifierForVendor.UUIDString};
137 | [_sio emit:@"register" with:@[dic]];
138 | }];
139 |
140 | [_sio on:@"disconnect" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
141 | _serverConnected = NO;
142 |
143 | [_userManager removeAllUsers];
144 | [self.tableView reloadData];
145 | }];
146 |
147 | [_sio on:@"register succeed" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
148 | //get self back
149 | NSLog(@"self is %@", data);
150 | [_userManager setLocalUserWithName:[[data lastObject] objectForKey:@"name"]
151 | UID:[[data lastObject] objectForKey:@"id"]];
152 |
153 | [self getOnlineContacts];
154 | }];
155 |
156 | [_sio on:@"chat message" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
157 | NSLog(@"message received : %@", data);
158 | [self handleNewMessage:[data lastObject]];
159 | }];
160 |
161 | [_sio on:@"new user" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
162 | [_userManager addUserWithUID:[[data lastObject] objectForKey:@"id"]
163 | name:[[data lastObject] objectForKey:@"name"]];
164 |
165 | //reload contacts
166 | [self getOnlineContacts];
167 | }];
168 |
169 | [_sio on:@"user leave" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ack) {
170 | //reload contacts
171 |
172 | //TODO delete chat session with this user
173 | if ([data count] > 0) {
174 | [self deleteChatSessionWithUser: [data lastObject]];
175 |
176 | [self getOnlineContacts];
177 | }
178 | }];
179 |
180 | [_sio connect];
181 | }
182 |
183 | - (void)deleteChatSessionWithUser : (id)userDic{
184 | NSString *uid = [userDic objectForKey:@"id"];
185 | UIViewController *topVc = self.navigationController.topViewController;
186 | if ([topVc isKindOfClass:[ChatViewController class]]) {
187 | //in chat view
188 | ChatViewController *chatVc = (ChatViewController *)topVc;
189 | if ([chatVc.peer.uniqueID isEqualToString: uid]) {
190 | //this is the exact session i'm talking in
191 | [self.navigationController popViewControllerAnimated:YES];
192 | }else{
193 | //i'm in a different chat session
194 | }
195 | }else{
196 | //in contacts view
197 | }
198 |
199 | [_userManager removeUserByUID:uid];
200 | }
201 |
202 | - (void)sendMessage : (Message *)message{
203 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
204 | [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
205 | message.time = [formatter stringFromDate:[NSDate date]];;
206 |
207 | [_sio emit:@"chat message" with:@[[message toDictionary]]];
208 | }
209 |
210 | - (void)handleNewMessage : (id)data{
211 | NSDictionary *dic;
212 | if ([data isKindOfClass:[NSDictionary class]]) {
213 | //data is dic
214 | dic = data;
215 | }else if([data isKindOfClass:[NSString class]]){
216 | //data is string
217 | dic = [NSJSONSerialization JSONObjectWithData:[data dataUsingEncoding:NSUTF8StringEncoding]
218 | options:NSJSONReadingAllowFragments
219 | error:nil];
220 | }
221 |
222 | Message *message = [[Message alloc] init];
223 | message.from = [dic objectForKey:@"from"];
224 | message.to = [dic objectForKey:@"to"];
225 | message.content = [dic objectForKey:@"content"];
226 | message.time = [dic objectForKey:@"time"];
227 | message.type = [dic objectForKey:@"type"];
228 | message.subtype = [dic objectForKey:@"subtype"];
229 |
230 | if (![message.to isEqualToString:[_userManager localUser].uniqueID] &&
231 | ![message.to isEqualToString:@"all"]) {
232 | //not my message
233 | return;
234 | }
235 |
236 | if ([message.type isEqualToString:@"signal"]) {
237 | [self handleSignalMessage : message];
238 | }else if([message.type isEqualToString:@"text"]){
239 | [self handleTextMessage:message];
240 | }
241 | }
242 |
243 | - (void)handleSignalMessage : (Message *)message{
244 | if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
245 | if ([message.subtype isEqualToString:@"offer"]) {
246 | //got offer in background
247 | [self handleIncomingCallInBackground : message];
248 | }
249 | }else{
250 | if ([message.subtype isEqualToString:@"offer"]) {
251 | //got offer
252 | [self presentIncomingCallViewController:message];
253 | }else if ([message.subtype isEqualToString:@"candidate"] ||
254 | [message.subtype isEqualToString:@"answer"] ||
255 | [message.subtype isEqualToString:@"close"]){
256 | //handle candidate when IncomingCallVC is not created yet.
257 | UIViewController *vc = self.presentedViewController;
258 | if ([vc isKindOfClass:[IncomingCallViewController class]]) {
259 | IncomingCallViewController *icvc = (IncomingCallViewController *)vc;
260 | [icvc onMessage:message];
261 | }else if([vc isKindOfClass:[VoiceCallViewController class]]){
262 | VoiceCallViewController *vcvc = (VoiceCallViewController *)vc;
263 | [vcvc onMessage:message];
264 | }else if([vc isKindOfClass:[VideoCallViewController class]]){
265 | VideoCallViewController *vcvc = (VideoCallViewController *)vc;
266 | [vcvc onMessage:message];
267 | }else{
268 | //received candidates when calling view not presented
269 | [_unReadSignalingMessages addObject:message];
270 | }
271 | }
272 | }
273 | }
274 |
275 | - (void)handleTextMessage : (Message *)message{
276 | if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
277 | [self handleBackgroundMessage: message];
278 | }else{
279 | //in chat view controller
280 | UIViewController *topVc = self.navigationController.topViewController;
281 | if ([topVc isKindOfClass:[ChatViewController class]]) {
282 | //in chat view
283 | ChatViewController *chatVc = (ChatViewController *)topVc;
284 | if ([chatVc.peer.uniqueID isEqualToString: message.from]) {
285 | //this is the exact session i'm talking in
286 | [chatVc onMessage:message];
287 | }else{
288 | //i'm in a different chat session
289 | [self handleUnReadMessage:message];
290 | }
291 | }else{
292 | //in contacts view
293 | [self handleUnReadMessage:message];
294 | }
295 | }
296 | }
297 |
298 | - (void)handleUnReadMessage : (Message *)message{
299 | //TODO update tableview cell status, and store this message to db.
300 | User *user = [_userManager findUserByUID:message.from];
301 | ChatSession *session = [_sessionManager createSessionWithPeer:user];
302 |
303 | [session onUnreadMessage:message];
304 |
305 | if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
306 | dispatch_async(dispatch_get_main_queue(), ^{
307 | [self markChatCellUnread : session];
308 | });
309 | }
310 | }
311 |
312 | - (void)markChatCellUnread : (ChatSession *)session{
313 | [self.tableView reloadData];
314 |
315 | //why refresh a single cell not working?
316 | /*
317 | UITableViewCell *cell = [self cellForChatSession:session];
318 | cell.tintColor = [UIColor redColor];
319 | cell.detailTextLabel.text = [NSString stringWithFormat:@"%lu unread", (unsigned long)[session unreadCount]];
320 | // [cell reloadInputViews];
321 | [self.tableView beginUpdates];
322 | [self.tableView reloadRowsAtIndexPaths:@[[self.tableView indexPathForCell:cell]] withRowAnimation:UITableViewRowAnimationAutomatic];
323 | [self.tableView endUpdates];
324 | */
325 | }
326 |
327 | - (UITableViewCell *)cellForChatSession : (ChatSession *)session{
328 | User *peer = session.peer;
329 | NSUInteger index = [[_userManager listUsers] indexOfObject:peer];
330 | return [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
331 | }
332 |
333 | - (void)getOnlineContacts{
334 | //restful
335 | NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:3000/listUsers", _hostAddr]];
336 | NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
337 | completionHandler:^(NSData * _Nullable data,
338 | NSURLResponse * _Nullable response,
339 | NSError * _Nullable error) {
340 | //jsonlize data
341 | if (error) {
342 | NSLog(@"%@", error);
343 | }else{
344 | NSError *error2 = nil;
345 | NSArray *users = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error2];
346 | if (error2) {
347 | NSLog(@"error jsonserialize : %@", error2);
348 | }
349 | NSLog(@"all users: %@", users);
350 |
351 | NSMutableArray *userArray = [NSMutableArray array];
352 | for (NSDictionary *item in users) {
353 | [userArray addObject:[[User alloc] initWithName:[item objectForKey:@"name"] UID:[item objectForKey:@"id"]]];
354 | }
355 | [[UserManager sharedManager] replaceAllUsersWithNewUsers:userArray];
356 |
357 | dispatch_async(dispatch_get_main_queue(), ^{
358 | [self.tableView reloadData];
359 | });
360 |
361 | }
362 | }];
363 |
364 | [task resume];
365 | }
366 |
367 | - (void)handleBackgroundMessage : (Message *)message{
368 | User *user = [_userManager findUserByUID:message.from];
369 | ChatSession *session = [_sessionManager findSessionByPeer:user];
370 | [session onUnreadMessage:message];
371 |
372 | NSString *notificationString = [NSString stringWithFormat:@"%@:%@", user.name, message.content];
373 | [self showLocalNotification: notificationString];
374 | }
375 |
376 | - (void)handleIncomingCallInBackground : (Message *)message{
377 | User *user = [_userManager findUserByUID:message.from];
378 |
379 | NSString *notificationString = [NSString stringWithFormat:@"%@ is calling you", user.name];
380 | [self showLocalNotification: notificationString];
381 | }
382 |
383 | - (void)showLocalNotification : (NSString *)message {
384 | NSLog(@"showLocalNotification");
385 |
386 | UILocalNotification *notification = [[UILocalNotification alloc] init];
387 | // notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:7];
388 | notification.alertBody = message;
389 | notification.timeZone = [NSTimeZone defaultTimeZone];
390 | notification.soundName = UILocalNotificationDefaultSoundName;
391 | notification.applicationIconBadgeNumber = [UIApplication sharedApplication].applicationIconBadgeNumber + 1;
392 |
393 | [[UIApplication sharedApplication] scheduleLocalNotification:notification];
394 | }
395 |
396 |
397 | - (IBAction)LeftBarButtonPressed:(id)sender{
398 | if (_serverConnected) {
399 | //refresh contacts
400 | }else{
401 | //show text inputview
402 | [self showServerInputView];
403 | }
404 | }
405 |
406 | #pragma mark - Table view data source
407 |
408 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
409 | return 1;
410 | }
411 |
412 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
413 | NSUInteger numberUsers = [_userManager numberUsers];
414 | self.footerLabel.text = [NSString stringWithFormat:@"total %lu users", (unsigned long)numberUsers];
415 | return numberUsers;
416 | }
417 |
418 |
419 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
420 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContactsTableViewCell" forIndexPath:indexPath];
421 | NSArray *users = [_userManager listUsers];
422 |
423 | // Configure the cell...
424 | User *user = [users objectAtIndex:indexPath.row];
425 | NSString *name = user.name;
426 | cell.textLabel.text = name;
427 | cell.detailTextLabel.text = nil;
428 | cell.accessoryView = nil;
429 |
430 | if ([[users objectAtIndex:indexPath.row].uniqueID isEqualToString: [_userManager localUser].uniqueID]) {
431 | cell.detailTextLabel.text = @"me";
432 | }else{
433 | ChatSession *session = [_sessionManager createSessionWithPeer:user];
434 | if (session.unreadCount > 0) {
435 | NSString *title = [NSString stringWithFormat:@"%lu", (unsigned long)(([session unreadCount] < 100) ? [session unreadCount] : 99)];
436 | UIButton *detailButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
437 | [detailButton setTitle:title forState:UIControlStateNormal];
438 | [detailButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
439 | [detailButton setContentHorizontalAlignment:UIControlContentHorizontalAlignmentCenter];
440 | [detailButton addTarget:self action:@selector(detailButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
441 | CGFloat height = [tableView rectForRowAtIndexPath:indexPath].size.height * 0.3;
442 | // CGFloat width = [tableView rectForRowAtIndexPath:indexPath].size.width * 0.4;
443 | CGFloat width = height;
444 |
445 | detailButton.frame = CGRectMake(0, 0, width, height);
446 | detailButton.tag = indexPath.row;
447 | detailButton.backgroundColor = [UIColor redColor];
448 | detailButton.layer.cornerRadius = height / 2;
449 | detailButton.layer.masksToBounds = YES;
450 |
451 | cell.accessoryView = detailButton;
452 | }
453 | }
454 |
455 | return cell;
456 | }
457 |
458 | - (void)detailButtonPressed : (id)sender{
459 | UIButton *button = (UIButton *)sender;
460 |
461 | NSLog(@"button in row %ld pressed", (long)button.tag);
462 | User *selectedUser = [[_userManager listUsers] objectAtIndex:button.tag];
463 |
464 | [self presentChatViewControllerWithPeer:selectedUser];
465 | }
466 |
467 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
468 | //show action sheet
469 | User *selectedUser = [[_userManager listUsers] objectAtIndex:indexPath.row];
470 | if (!selectedUser) {
471 | return;
472 | }
473 | if ([selectedUser.uniqueID isEqualToString: [_userManager localUser].uniqueID]) {
474 | //do nothing on click myself
475 | return;
476 | }
477 |
478 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Action"
479 | message:nil
480 | preferredStyle:UIAlertControllerStyleActionSheet];
481 | [alert addAction:[UIAlertAction actionWithTitle:@"Chat"
482 | style:UIAlertActionStyleDefault
483 | handler:^(UIAlertAction * _Nonnull action) {
484 | //chat selected
485 | [self presentChatViewControllerWithPeer:selectedUser];
486 | }]];
487 | [alert addAction:[UIAlertAction actionWithTitle:@"Voice Chat"
488 | style:UIAlertActionStyleDefault
489 | handler:^(UIAlertAction * _Nonnull action) {
490 | //Voice chat selected
491 | [self presentVoiceCallViewControllerWithPeer:selectedUser];
492 | }]];
493 | [alert addAction:[UIAlertAction actionWithTitle:@"Video Chat"
494 | style:UIAlertActionStyleDefault
495 | handler:^(UIAlertAction * _Nonnull action) {
496 | //Video Chat selected
497 | [self presentVideoCallViewControllerWithPeer:selectedUser];
498 | }]];
499 | [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
500 |
501 | //If call presentViewController directly, there will be several seconds lag before actionsheet actually popup
502 | dispatch_async(dispatch_get_main_queue(), ^{
503 | [self presentViewController:alert animated:YES completion:nil];
504 | });
505 | }
506 |
507 | - (void)presentChatViewControllerWithPeer: (User *)user{
508 | UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
509 | ChatViewController *cvc = [sb instantiateViewControllerWithIdentifier:@"ChatViewController"];
510 | cvc.socketIODelegate = self;
511 | cvc.peer = user;
512 | cvc.title = @"Chat";
513 | [self.navigationController pushViewController:cvc animated:YES];
514 | }
515 |
516 | - (void)presentVoiceCallViewControllerWithPeer: (User *)user{
517 | UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
518 | VoiceCallViewController *vcvc = [sb instantiateViewControllerWithIdentifier:@"VoiceCallViewController"];
519 | vcvc.socketIODelegate = self;
520 | vcvc.peer = user;
521 |
522 | [self presentViewController:vcvc animated:YES completion:nil];
523 | }
524 |
525 | - (void)presentVideoCallViewControllerWithPeer: (User *)user{
526 | UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
527 | VideoCallViewController *vcvc = [sb instantiateViewControllerWithIdentifier:@"VideoCallViewController"];
528 | vcvc.socketIODelegate = self;
529 | vcvc.peer = user;
530 |
531 | [self presentViewController:vcvc animated:YES completion:nil];
532 | }
533 |
534 | - (void)presentIncomingCallViewController : (Message *)message{
535 | User *peer = [_userManager findUserByUID:message.from];
536 |
537 | UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
538 | IncomingCallViewController *icvc = [sb instantiateViewControllerWithIdentifier:@"IncomingCallViewController"];
539 | icvc.socketIODelegate = self;
540 | icvc.peer = peer;
541 | icvc.offer = message;
542 |
543 | [self presentViewController:icvc animated:YES completion:^(void){
544 | icvc.pendingMessages = _unReadSignalingMessages;
545 | }];
546 |
547 | }
548 |
549 | - (void)updateSearchResultsForSearchController:(UISearchController *)searchController{
550 | if (!searchController.active) {
551 | return;
552 | }
553 |
554 | NSString *searchText = searchController.searchBar.text;
555 | NSLog(@"search text: %@", searchText);
556 | }
557 |
558 | /*
559 | // Override to support conditional editing of the table view.
560 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
561 | // Return NO if you do not want the specified item to be editable.
562 | return YES;
563 | }
564 | */
565 |
566 | /*
567 | // Override to support editing the table view.
568 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
569 | if (editingStyle == UITableViewCellEditingStyleDelete) {
570 | // Delete the row from the data source
571 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
572 | } else if (editingStyle == UITableViewCellEditingStyleInsert) {
573 | // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
574 | }
575 | }
576 | */
577 |
578 | /*
579 | // Override to support rearranging the table view.
580 | - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
581 | }
582 | */
583 |
584 | /*
585 | // Override to support conditional rearranging of the table view.
586 | - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
587 | // Return NO if you do not want the item to be re-orderable.
588 | return YES;
589 | }
590 | */
591 |
592 | /*
593 | #pragma mark - Navigation
594 |
595 | // In a storyboard-based application, you will often want to do a little preparation before navigation
596 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
597 | // Get the new view controller using [segue destinationViewController].
598 | // Pass the selected object to the new view controller.
599 | }
600 | */
601 |
602 | @end
603 |
--------------------------------------------------------------------------------
/Chatchat.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 02BE6DA0AD957EECBA07E7C8 /* Pods_Chatchat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD4431605C4967AED9A7A9BE /* Pods_Chatchat.framework */; };
11 | 1A03B0851D06CFE200E72938 /* VoiceCallViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A03B0841D06CFE200E72938 /* VoiceCallViewController.m */; };
12 | 1A03B09C1D08120700E72938 /* RTCICECandidate+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A03B09B1D08120700E72938 /* RTCICECandidate+JSON.m */; };
13 | 1A03B09F1D08198F00E72938 /* IncomingCallViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A03B09E1D08198F00E72938 /* IncomingCallViewController.m */; };
14 | 1A402FB51E00DE220000E020 /* RTCSessionDescription+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A402FB41E00DE220000E020 /* RTCSessionDescription+JSON.m */; };
15 | 1A402FB81E0120A70000E020 /* ARDSDPUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A402FB71E0120A70000E020 /* ARDSDPUtils.m */; };
16 | 1A8EBF381CFD38930037BABB /* ContactsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8EBF371CFD38930037BABB /* ContactsTableViewController.m */; };
17 | 1A8EBF421CFE80420037BABB /* ChatViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8EBF411CFE80410037BABB /* ChatViewController.m */; };
18 | 1A8EBF461CFECC540037BABB /* User.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8EBF451CFECC540037BABB /* User.m */; };
19 | 1A8EBF491CFECC640037BABB /* Message.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8EBF481CFECC640037BABB /* Message.m */; };
20 | 1A8EBF4C1CFFDB080037BABB /* ChatSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8EBF4B1CFFDB040037BABB /* ChatSession.m */; };
21 | 1A8EBF4F1CFFDF9C0037BABB /* UserManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8EBF4E1CFFDF9C0037BABB /* UserManager.m */; };
22 | 1A8EBF521CFFE3450037BABB /* ChatSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8EBF511CFFE3450037BABB /* ChatSessionManager.m */; };
23 | 1AB9EC671D33443200BCC48A /* CallViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB9EC661D33443200BCC48A /* CallViewController.m */; };
24 | 1AB9EC6A1D3350DC00BCC48A /* OutgoingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB9EC691D3350DC00BCC48A /* OutgoingViewController.m */; };
25 | 1ABCAACA1D1D051A0087E1CA /* VideoCallViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABCAAC91D1D05190087E1CA /* VideoCallViewController.m */; };
26 | B2CDF06B1CF6DDBA00FCECA8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CDF06A1CF6DDBA00FCECA8 /* main.m */; };
27 | B2CDF06E1CF6DDBA00FCECA8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CDF06D1CF6DDBA00FCECA8 /* AppDelegate.m */; };
28 | B2CDF0741CF6DDBB00FCECA8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B2CDF0721CF6DDBB00FCECA8 /* Main.storyboard */; };
29 | B2CDF0761CF6DDBB00FCECA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B2CDF0751CF6DDBB00FCECA8 /* Assets.xcassets */; };
30 | B2CDF0791CF6DDBB00FCECA8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B2CDF0771CF6DDBB00FCECA8 /* LaunchScreen.storyboard */; };
31 | /* End PBXBuildFile section */
32 |
33 | /* Begin PBXCopyFilesBuildPhase section */
34 | 1A7874B61DFA7B9600302C59 /* Embed Frameworks */ = {
35 | isa = PBXCopyFilesBuildPhase;
36 | buildActionMask = 2147483647;
37 | dstPath = "";
38 | dstSubfolderSpec = 10;
39 | files = (
40 | );
41 | name = "Embed Frameworks";
42 | runOnlyForDeploymentPostprocessing = 0;
43 | };
44 | /* End PBXCopyFilesBuildPhase section */
45 |
46 | /* Begin PBXFileReference section */
47 | 1A03B0831D06CFE200E72938 /* VoiceCallViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VoiceCallViewController.h; sourceTree = ""; };
48 | 1A03B0841D06CFE200E72938 /* VoiceCallViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceCallViewController.m; sourceTree = ""; };
49 | 1A03B09A1D08120700E72938 /* RTCICECandidate+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RTCICECandidate+JSON.h"; sourceTree = ""; };
50 | 1A03B09B1D08120700E72938 /* RTCICECandidate+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RTCICECandidate+JSON.m"; sourceTree = ""; };
51 | 1A03B09D1D08198F00E72938 /* IncomingCallViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IncomingCallViewController.h; sourceTree = ""; };
52 | 1A03B09E1D08198F00E72938 /* IncomingCallViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IncomingCallViewController.m; sourceTree = ""; };
53 | 1A402FB31E00DE220000E020 /* RTCSessionDescription+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RTCSessionDescription+JSON.h"; sourceTree = ""; };
54 | 1A402FB41E00DE220000E020 /* RTCSessionDescription+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RTCSessionDescription+JSON.m"; sourceTree = ""; };
55 | 1A402FB61E0120A70000E020 /* ARDSDPUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARDSDPUtils.h; sourceTree = ""; };
56 | 1A402FB71E0120A70000E020 /* ARDSDPUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARDSDPUtils.m; sourceTree = ""; };
57 | 1A8EBF361CFD38930037BABB /* ContactsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsTableViewController.h; sourceTree = ""; };
58 | 1A8EBF371CFD38930037BABB /* ContactsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactsTableViewController.m; sourceTree = ""; };
59 | 1A8EBF401CFE80410037BABB /* ChatViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatViewController.h; sourceTree = ""; };
60 | 1A8EBF411CFE80410037BABB /* ChatViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatViewController.m; sourceTree = ""; };
61 | 1A8EBF431CFEB0C40037BABB /* CommonDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CommonDefines.h; sourceTree = ""; };
62 | 1A8EBF441CFECC540037BABB /* User.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = User.h; sourceTree = ""; };
63 | 1A8EBF451CFECC540037BABB /* User.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = User.m; sourceTree = ""; };
64 | 1A8EBF471CFECC640037BABB /* Message.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Message.h; sourceTree = ""; };
65 | 1A8EBF481CFECC640037BABB /* Message.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Message.m; sourceTree = ""; };
66 | 1A8EBF4A1CFFDB040037BABB /* ChatSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatSession.h; sourceTree = ""; };
67 | 1A8EBF4B1CFFDB040037BABB /* ChatSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatSession.m; sourceTree = ""; };
68 | 1A8EBF4D1CFFDF9C0037BABB /* UserManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserManager.h; sourceTree = ""; };
69 | 1A8EBF4E1CFFDF9C0037BABB /* UserManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserManager.m; sourceTree = ""; };
70 | 1A8EBF501CFFE3450037BABB /* ChatSessionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatSessionManager.h; sourceTree = ""; };
71 | 1A8EBF511CFFE3450037BABB /* ChatSessionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatSessionManager.m; sourceTree = ""; };
72 | 1AB9EC651D33443200BCC48A /* CallViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallViewController.h; sourceTree = ""; };
73 | 1AB9EC661D33443200BCC48A /* CallViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallViewController.m; sourceTree = ""; };
74 | 1AB9EC681D3350DC00BCC48A /* OutgoingViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OutgoingViewController.h; sourceTree = ""; };
75 | 1AB9EC691D3350DC00BCC48A /* OutgoingViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OutgoingViewController.m; sourceTree = ""; };
76 | 1ABCAAC81D1D05190087E1CA /* VideoCallViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoCallViewController.h; sourceTree = ""; };
77 | 1ABCAAC91D1D05190087E1CA /* VideoCallViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoCallViewController.m; sourceTree = ""; };
78 | 23B859805FBE0EB62D7114E0 /* Pods-Chatchat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chatchat.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Chatchat/Pods-Chatchat.debug.xcconfig"; sourceTree = ""; };
79 | A50343BB10F4589FD3C9335A /* Pods-Chatchat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chatchat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Chatchat/Pods-Chatchat.release.xcconfig"; sourceTree = ""; };
80 | B2CDF0661CF6DDBA00FCECA8 /* Chatchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Chatchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
81 | B2CDF06A1CF6DDBA00FCECA8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
82 | B2CDF06C1CF6DDBA00FCECA8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
83 | B2CDF06D1CF6DDBA00FCECA8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
84 | B2CDF0731CF6DDBB00FCECA8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
85 | B2CDF0751CF6DDBB00FCECA8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
86 | B2CDF0781CF6DDBB00FCECA8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
87 | B2CDF07A1CF6DDBB00FCECA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
88 | B2CDF0AE1CF6EF1F00FCECA8 /* constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = constants.h; sourceTree = ""; };
89 | BD4431605C4967AED9A7A9BE /* Pods_Chatchat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chatchat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
90 | /* End PBXFileReference section */
91 |
92 | /* Begin PBXFrameworksBuildPhase section */
93 | B2CDF0631CF6DDBA00FCECA8 /* Frameworks */ = {
94 | isa = PBXFrameworksBuildPhase;
95 | buildActionMask = 2147483647;
96 | files = (
97 | 02BE6DA0AD957EECBA07E7C8 /* Pods_Chatchat.framework in Frameworks */,
98 | );
99 | runOnlyForDeploymentPostprocessing = 0;
100 | };
101 | /* End PBXFrameworksBuildPhase section */
102 |
103 | /* Begin PBXGroup section */
104 | 1A1F050E1D05043C00982A1E /* ViewControllers */ = {
105 | isa = PBXGroup;
106 | children = (
107 | 1A8EBF361CFD38930037BABB /* ContactsTableViewController.h */,
108 | 1A8EBF371CFD38930037BABB /* ContactsTableViewController.m */,
109 | 1A8EBF401CFE80410037BABB /* ChatViewController.h */,
110 | 1A8EBF411CFE80410037BABB /* ChatViewController.m */,
111 | 1AB9EC651D33443200BCC48A /* CallViewController.h */,
112 | 1AB9EC661D33443200BCC48A /* CallViewController.m */,
113 | 1AB9EC681D3350DC00BCC48A /* OutgoingViewController.h */,
114 | 1AB9EC691D3350DC00BCC48A /* OutgoingViewController.m */,
115 | 1A03B0831D06CFE200E72938 /* VoiceCallViewController.h */,
116 | 1A03B0841D06CFE200E72938 /* VoiceCallViewController.m */,
117 | 1ABCAAC81D1D05190087E1CA /* VideoCallViewController.h */,
118 | 1ABCAAC91D1D05190087E1CA /* VideoCallViewController.m */,
119 | 1A03B09D1D08198F00E72938 /* IncomingCallViewController.h */,
120 | 1A03B09E1D08198F00E72938 /* IncomingCallViewController.m */,
121 | 1A03B09A1D08120700E72938 /* RTCICECandidate+JSON.h */,
122 | 1A03B09B1D08120700E72938 /* RTCICECandidate+JSON.m */,
123 | 1A402FB31E00DE220000E020 /* RTCSessionDescription+JSON.h */,
124 | 1A402FB41E00DE220000E020 /* RTCSessionDescription+JSON.m */,
125 | );
126 | name = ViewControllers;
127 | sourceTree = "";
128 | };
129 | 1A1F050F1D05044F00982A1E /* Models */ = {
130 | isa = PBXGroup;
131 | children = (
132 | 1A8EBF4A1CFFDB040037BABB /* ChatSession.h */,
133 | 1A8EBF4B1CFFDB040037BABB /* ChatSession.m */,
134 | 1A8EBF501CFFE3450037BABB /* ChatSessionManager.h */,
135 | 1A8EBF511CFFE3450037BABB /* ChatSessionManager.m */,
136 | 1A8EBF441CFECC540037BABB /* User.h */,
137 | 1A8EBF451CFECC540037BABB /* User.m */,
138 | 1A8EBF4D1CFFDF9C0037BABB /* UserManager.h */,
139 | 1A8EBF4E1CFFDF9C0037BABB /* UserManager.m */,
140 | 1A8EBF471CFECC640037BABB /* Message.h */,
141 | 1A8EBF481CFECC640037BABB /* Message.m */,
142 | );
143 | name = Models;
144 | sourceTree = "";
145 | };
146 | 96D1BA9E62D8039829C7B42F /* Frameworks */ = {
147 | isa = PBXGroup;
148 | children = (
149 | BD4431605C4967AED9A7A9BE /* Pods_Chatchat.framework */,
150 | );
151 | name = Frameworks;
152 | sourceTree = "";
153 | };
154 | B2CDF05D1CF6DDBA00FCECA8 = {
155 | isa = PBXGroup;
156 | children = (
157 | B2CDF0681CF6DDBA00FCECA8 /* Chatchat */,
158 | B2CDF0671CF6DDBA00FCECA8 /* Products */,
159 | E8B54AA66B3734C91B4449B3 /* Pods */,
160 | 96D1BA9E62D8039829C7B42F /* Frameworks */,
161 | );
162 | sourceTree = "";
163 | };
164 | B2CDF0671CF6DDBA00FCECA8 /* Products */ = {
165 | isa = PBXGroup;
166 | children = (
167 | B2CDF0661CF6DDBA00FCECA8 /* Chatchat.app */,
168 | );
169 | name = Products;
170 | sourceTree = "";
171 | };
172 | B2CDF0681CF6DDBA00FCECA8 /* Chatchat */ = {
173 | isa = PBXGroup;
174 | children = (
175 | 1A1F050F1D05044F00982A1E /* Models */,
176 | 1A1F050E1D05043C00982A1E /* ViewControllers */,
177 | B2CDF06C1CF6DDBA00FCECA8 /* AppDelegate.h */,
178 | B2CDF06D1CF6DDBA00FCECA8 /* AppDelegate.m */,
179 | 1A402FB61E0120A70000E020 /* ARDSDPUtils.h */,
180 | 1A402FB71E0120A70000E020 /* ARDSDPUtils.m */,
181 | B2CDF0AE1CF6EF1F00FCECA8 /* constants.h */,
182 | 1A8EBF431CFEB0C40037BABB /* CommonDefines.h */,
183 | B2CDF0721CF6DDBB00FCECA8 /* Main.storyboard */,
184 | B2CDF0751CF6DDBB00FCECA8 /* Assets.xcassets */,
185 | B2CDF0771CF6DDBB00FCECA8 /* LaunchScreen.storyboard */,
186 | B2CDF07A1CF6DDBB00FCECA8 /* Info.plist */,
187 | B2CDF0691CF6DDBA00FCECA8 /* Supporting Files */,
188 | );
189 | path = Chatchat;
190 | sourceTree = "";
191 | };
192 | B2CDF0691CF6DDBA00FCECA8 /* Supporting Files */ = {
193 | isa = PBXGroup;
194 | children = (
195 | B2CDF06A1CF6DDBA00FCECA8 /* main.m */,
196 | );
197 | name = "Supporting Files";
198 | sourceTree = "";
199 | };
200 | E8B54AA66B3734C91B4449B3 /* Pods */ = {
201 | isa = PBXGroup;
202 | children = (
203 | 23B859805FBE0EB62D7114E0 /* Pods-Chatchat.debug.xcconfig */,
204 | A50343BB10F4589FD3C9335A /* Pods-Chatchat.release.xcconfig */,
205 | );
206 | name = Pods;
207 | sourceTree = "";
208 | };
209 | /* End PBXGroup section */
210 |
211 | /* Begin PBXNativeTarget section */
212 | B2CDF0651CF6DDBA00FCECA8 /* Chatchat */ = {
213 | isa = PBXNativeTarget;
214 | buildConfigurationList = B2CDF07D1CF6DDBB00FCECA8 /* Build configuration list for PBXNativeTarget "Chatchat" */;
215 | buildPhases = (
216 | 419EBDE2A7F3C4E3B9357C59 /* [CP] Check Pods Manifest.lock */,
217 | 819A37B32909F05C63604591 /* [CP] Check Pods Manifest.lock */,
218 | B2CDF0621CF6DDBA00FCECA8 /* Sources */,
219 | B2CDF0631CF6DDBA00FCECA8 /* Frameworks */,
220 | B2CDF0641CF6DDBA00FCECA8 /* Resources */,
221 | 939835C133C68AFEC6F923A7 /* [CP] Embed Pods Frameworks */,
222 | 007566F4BE525EF9774EDFD6 /* [CP] Copy Pods Resources */,
223 | 4D52EC991DEE53D50E3FC9A0 /* 📦 Embed Pods Frameworks */,
224 | A75C17AF99809625482B01FE /* 📦 Copy Pods Resources */,
225 | 1A7874B61DFA7B9600302C59 /* Embed Frameworks */,
226 | );
227 | buildRules = (
228 | );
229 | dependencies = (
230 | );
231 | name = Chatchat;
232 | productName = Chatchat;
233 | productReference = B2CDF0661CF6DDBA00FCECA8 /* Chatchat.app */;
234 | productType = "com.apple.product-type.application";
235 | };
236 | /* End PBXNativeTarget section */
237 |
238 | /* Begin PBXProject section */
239 | B2CDF05E1CF6DDBA00FCECA8 /* Project object */ = {
240 | isa = PBXProject;
241 | attributes = {
242 | LastUpgradeCheck = 0720;
243 | ORGANIZATIONNAME = Beta.Inc;
244 | TargetAttributes = {
245 | B2CDF0651CF6DDBA00FCECA8 = {
246 | CreatedOnToolsVersion = 7.2.1;
247 | DevelopmentTeam = NY93BV4889;
248 | SystemCapabilities = {
249 | com.apple.BackgroundModes = {
250 | enabled = 1;
251 | };
252 | };
253 | };
254 | };
255 | };
256 | buildConfigurationList = B2CDF0611CF6DDBA00FCECA8 /* Build configuration list for PBXProject "Chatchat" */;
257 | compatibilityVersion = "Xcode 3.2";
258 | developmentRegion = English;
259 | hasScannedForEncodings = 0;
260 | knownRegions = (
261 | en,
262 | Base,
263 | );
264 | mainGroup = B2CDF05D1CF6DDBA00FCECA8;
265 | productRefGroup = B2CDF0671CF6DDBA00FCECA8 /* Products */;
266 | projectDirPath = "";
267 | projectRoot = "";
268 | targets = (
269 | B2CDF0651CF6DDBA00FCECA8 /* Chatchat */,
270 | );
271 | };
272 | /* End PBXProject section */
273 |
274 | /* Begin PBXResourcesBuildPhase section */
275 | B2CDF0641CF6DDBA00FCECA8 /* Resources */ = {
276 | isa = PBXResourcesBuildPhase;
277 | buildActionMask = 2147483647;
278 | files = (
279 | B2CDF0791CF6DDBB00FCECA8 /* LaunchScreen.storyboard in Resources */,
280 | B2CDF0761CF6DDBB00FCECA8 /* Assets.xcassets in Resources */,
281 | B2CDF0741CF6DDBB00FCECA8 /* Main.storyboard in Resources */,
282 | );
283 | runOnlyForDeploymentPostprocessing = 0;
284 | };
285 | /* End PBXResourcesBuildPhase section */
286 |
287 | /* Begin PBXShellScriptBuildPhase section */
288 | 007566F4BE525EF9774EDFD6 /* [CP] Copy Pods Resources */ = {
289 | isa = PBXShellScriptBuildPhase;
290 | buildActionMask = 2147483647;
291 | files = (
292 | );
293 | inputPaths = (
294 | );
295 | name = "[CP] Copy Pods Resources";
296 | outputPaths = (
297 | );
298 | runOnlyForDeploymentPostprocessing = 0;
299 | shellPath = /bin/sh;
300 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Chatchat/Pods-Chatchat-resources.sh\"\n";
301 | showEnvVarsInLog = 0;
302 | };
303 | 419EBDE2A7F3C4E3B9357C59 /* [CP] Check Pods Manifest.lock */ = {
304 | isa = PBXShellScriptBuildPhase;
305 | buildActionMask = 2147483647;
306 | files = (
307 | );
308 | inputPaths = (
309 | );
310 | name = "[CP] Check Pods Manifest.lock";
311 | outputPaths = (
312 | );
313 | runOnlyForDeploymentPostprocessing = 0;
314 | shellPath = /bin/sh;
315 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
316 | showEnvVarsInLog = 0;
317 | };
318 | 4D52EC991DEE53D50E3FC9A0 /* 📦 Embed Pods Frameworks */ = {
319 | isa = PBXShellScriptBuildPhase;
320 | buildActionMask = 2147483647;
321 | files = (
322 | );
323 | inputPaths = (
324 | );
325 | name = "📦 Embed Pods Frameworks";
326 | outputPaths = (
327 | );
328 | runOnlyForDeploymentPostprocessing = 0;
329 | shellPath = /bin/sh;
330 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Chatchat/Pods-Chatchat-frameworks.sh\"\n";
331 | showEnvVarsInLog = 0;
332 | };
333 | 819A37B32909F05C63604591 /* [CP] Check Pods Manifest.lock */ = {
334 | isa = PBXShellScriptBuildPhase;
335 | buildActionMask = 2147483647;
336 | files = (
337 | );
338 | inputPaths = (
339 | );
340 | name = "[CP] Check Pods Manifest.lock";
341 | outputPaths = (
342 | );
343 | runOnlyForDeploymentPostprocessing = 0;
344 | shellPath = /bin/sh;
345 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
346 | showEnvVarsInLog = 0;
347 | };
348 | 939835C133C68AFEC6F923A7 /* [CP] Embed Pods Frameworks */ = {
349 | isa = PBXShellScriptBuildPhase;
350 | buildActionMask = 2147483647;
351 | files = (
352 | );
353 | inputPaths = (
354 | );
355 | name = "[CP] Embed Pods Frameworks";
356 | outputPaths = (
357 | );
358 | runOnlyForDeploymentPostprocessing = 0;
359 | shellPath = /bin/sh;
360 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Chatchat/Pods-Chatchat-frameworks.sh\"\n";
361 | showEnvVarsInLog = 0;
362 | };
363 | A75C17AF99809625482B01FE /* 📦 Copy Pods Resources */ = {
364 | isa = PBXShellScriptBuildPhase;
365 | buildActionMask = 2147483647;
366 | files = (
367 | );
368 | inputPaths = (
369 | );
370 | name = "📦 Copy Pods Resources";
371 | outputPaths = (
372 | );
373 | runOnlyForDeploymentPostprocessing = 0;
374 | shellPath = /bin/sh;
375 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Chatchat/Pods-Chatchat-resources.sh\"\n";
376 | showEnvVarsInLog = 0;
377 | };
378 | /* End PBXShellScriptBuildPhase section */
379 |
380 | /* Begin PBXSourcesBuildPhase section */
381 | B2CDF0621CF6DDBA00FCECA8 /* Sources */ = {
382 | isa = PBXSourcesBuildPhase;
383 | buildActionMask = 2147483647;
384 | files = (
385 | 1A8EBF461CFECC540037BABB /* User.m in Sources */,
386 | 1A8EBF521CFFE3450037BABB /* ChatSessionManager.m in Sources */,
387 | 1AB9EC6A1D3350DC00BCC48A /* OutgoingViewController.m in Sources */,
388 | 1A03B09F1D08198F00E72938 /* IncomingCallViewController.m in Sources */,
389 | 1A8EBF421CFE80420037BABB /* ChatViewController.m in Sources */,
390 | 1A03B09C1D08120700E72938 /* RTCICECandidate+JSON.m in Sources */,
391 | 1A8EBF381CFD38930037BABB /* ContactsTableViewController.m in Sources */,
392 | B2CDF06E1CF6DDBA00FCECA8 /* AppDelegate.m in Sources */,
393 | B2CDF06B1CF6DDBA00FCECA8 /* main.m in Sources */,
394 | 1A8EBF4F1CFFDF9C0037BABB /* UserManager.m in Sources */,
395 | 1A8EBF4C1CFFDB080037BABB /* ChatSession.m in Sources */,
396 | 1A8EBF491CFECC640037BABB /* Message.m in Sources */,
397 | 1A03B0851D06CFE200E72938 /* VoiceCallViewController.m in Sources */,
398 | 1A402FB51E00DE220000E020 /* RTCSessionDescription+JSON.m in Sources */,
399 | 1A402FB81E0120A70000E020 /* ARDSDPUtils.m in Sources */,
400 | 1AB9EC671D33443200BCC48A /* CallViewController.m in Sources */,
401 | 1ABCAACA1D1D051A0087E1CA /* VideoCallViewController.m in Sources */,
402 | );
403 | runOnlyForDeploymentPostprocessing = 0;
404 | };
405 | /* End PBXSourcesBuildPhase section */
406 |
407 | /* Begin PBXVariantGroup section */
408 | B2CDF0721CF6DDBB00FCECA8 /* Main.storyboard */ = {
409 | isa = PBXVariantGroup;
410 | children = (
411 | B2CDF0731CF6DDBB00FCECA8 /* Base */,
412 | );
413 | name = Main.storyboard;
414 | sourceTree = "";
415 | };
416 | B2CDF0771CF6DDBB00FCECA8 /* LaunchScreen.storyboard */ = {
417 | isa = PBXVariantGroup;
418 | children = (
419 | B2CDF0781CF6DDBB00FCECA8 /* Base */,
420 | );
421 | name = LaunchScreen.storyboard;
422 | sourceTree = "";
423 | };
424 | /* End PBXVariantGroup section */
425 |
426 | /* Begin XCBuildConfiguration section */
427 | B2CDF07B1CF6DDBB00FCECA8 /* Debug */ = {
428 | isa = XCBuildConfiguration;
429 | buildSettings = {
430 | ALWAYS_SEARCH_USER_PATHS = NO;
431 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
432 | CLANG_CXX_LIBRARY = "libc++";
433 | CLANG_ENABLE_MODULES = YES;
434 | CLANG_ENABLE_OBJC_ARC = YES;
435 | CLANG_WARN_BOOL_CONVERSION = YES;
436 | CLANG_WARN_CONSTANT_CONVERSION = YES;
437 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
438 | CLANG_WARN_EMPTY_BODY = YES;
439 | CLANG_WARN_ENUM_CONVERSION = YES;
440 | CLANG_WARN_INT_CONVERSION = YES;
441 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
442 | CLANG_WARN_UNREACHABLE_CODE = YES;
443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
444 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
445 | COPY_PHASE_STRIP = NO;
446 | DEBUG_INFORMATION_FORMAT = dwarf;
447 | ENABLE_STRICT_OBJC_MSGSEND = YES;
448 | ENABLE_TESTABILITY = YES;
449 | GCC_C_LANGUAGE_STANDARD = gnu99;
450 | GCC_DYNAMIC_NO_PIC = NO;
451 | GCC_NO_COMMON_BLOCKS = YES;
452 | GCC_OPTIMIZATION_LEVEL = 0;
453 | GCC_PREPROCESSOR_DEFINITIONS = (
454 | "DEBUG=1",
455 | "$(inherited)",
456 | );
457 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
458 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
459 | GCC_WARN_UNDECLARED_SELECTOR = YES;
460 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
461 | GCC_WARN_UNUSED_FUNCTION = YES;
462 | GCC_WARN_UNUSED_VARIABLE = YES;
463 | IPHONEOS_DEPLOYMENT_TARGET = 9.2;
464 | MTL_ENABLE_DEBUG_INFO = YES;
465 | ONLY_ACTIVE_ARCH = YES;
466 | SDKROOT = iphoneos;
467 | TARGETED_DEVICE_FAMILY = "1,2";
468 | };
469 | name = Debug;
470 | };
471 | B2CDF07C1CF6DDBB00FCECA8 /* Release */ = {
472 | isa = XCBuildConfiguration;
473 | buildSettings = {
474 | ALWAYS_SEARCH_USER_PATHS = NO;
475 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
476 | CLANG_CXX_LIBRARY = "libc++";
477 | CLANG_ENABLE_MODULES = YES;
478 | CLANG_ENABLE_OBJC_ARC = YES;
479 | CLANG_WARN_BOOL_CONVERSION = YES;
480 | CLANG_WARN_CONSTANT_CONVERSION = YES;
481 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
482 | CLANG_WARN_EMPTY_BODY = YES;
483 | CLANG_WARN_ENUM_CONVERSION = YES;
484 | CLANG_WARN_INT_CONVERSION = YES;
485 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
486 | CLANG_WARN_UNREACHABLE_CODE = YES;
487 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
488 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
489 | COPY_PHASE_STRIP = NO;
490 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
491 | ENABLE_NS_ASSERTIONS = NO;
492 | ENABLE_STRICT_OBJC_MSGSEND = YES;
493 | GCC_C_LANGUAGE_STANDARD = gnu99;
494 | GCC_NO_COMMON_BLOCKS = YES;
495 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
496 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
497 | GCC_WARN_UNDECLARED_SELECTOR = YES;
498 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
499 | GCC_WARN_UNUSED_FUNCTION = YES;
500 | GCC_WARN_UNUSED_VARIABLE = YES;
501 | IPHONEOS_DEPLOYMENT_TARGET = 9.2;
502 | MTL_ENABLE_DEBUG_INFO = NO;
503 | SDKROOT = iphoneos;
504 | TARGETED_DEVICE_FAMILY = "1,2";
505 | VALIDATE_PRODUCT = YES;
506 | };
507 | name = Release;
508 | };
509 | B2CDF07E1CF6DDBB00FCECA8 /* Debug */ = {
510 | isa = XCBuildConfiguration;
511 | baseConfigurationReference = 23B859805FBE0EB62D7114E0 /* Pods-Chatchat.debug.xcconfig */;
512 | buildSettings = {
513 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
514 | CODE_SIGN_IDENTITY = "iPhone Developer";
515 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
516 | ENABLE_BITCODE = NO;
517 | INFOPLIST_FILE = Chatchat/Info.plist;
518 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
519 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
520 | PRODUCT_BUNDLE_IDENTIFIER = Beta.Inc.Chatchat;
521 | PRODUCT_NAME = "$(TARGET_NAME)";
522 | PROVISIONING_PROFILE = "";
523 | };
524 | name = Debug;
525 | };
526 | B2CDF07F1CF6DDBB00FCECA8 /* Release */ = {
527 | isa = XCBuildConfiguration;
528 | baseConfigurationReference = A50343BB10F4589FD3C9335A /* Pods-Chatchat.release.xcconfig */;
529 | buildSettings = {
530 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
531 | CODE_SIGN_IDENTITY = "iPhone Developer";
532 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
533 | ENABLE_BITCODE = NO;
534 | INFOPLIST_FILE = Chatchat/Info.plist;
535 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
536 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
537 | PRODUCT_BUNDLE_IDENTIFIER = Beta.Inc.Chatchat;
538 | PRODUCT_NAME = "$(TARGET_NAME)";
539 | PROVISIONING_PROFILE = "";
540 | };
541 | name = Release;
542 | };
543 | /* End XCBuildConfiguration section */
544 |
545 | /* Begin XCConfigurationList section */
546 | B2CDF0611CF6DDBA00FCECA8 /* Build configuration list for PBXProject "Chatchat" */ = {
547 | isa = XCConfigurationList;
548 | buildConfigurations = (
549 | B2CDF07B1CF6DDBB00FCECA8 /* Debug */,
550 | B2CDF07C1CF6DDBB00FCECA8 /* Release */,
551 | );
552 | defaultConfigurationIsVisible = 0;
553 | defaultConfigurationName = Release;
554 | };
555 | B2CDF07D1CF6DDBB00FCECA8 /* Build configuration list for PBXNativeTarget "Chatchat" */ = {
556 | isa = XCConfigurationList;
557 | buildConfigurations = (
558 | B2CDF07E1CF6DDBB00FCECA8 /* Debug */,
559 | B2CDF07F1CF6DDBB00FCECA8 /* Release */,
560 | );
561 | defaultConfigurationIsVisible = 0;
562 | defaultConfigurationName = Release;
563 | };
564 | /* End XCConfigurationList section */
565 | };
566 | rootObject = B2CDF05E1CF6DDBA00FCECA8 /* Project object */;
567 | }
568 |
--------------------------------------------------------------------------------
/Chatchat/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
44 |
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 |
111 |
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 |
201 |
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 |
245 |
252 |
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 |
297 |
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 |
--------------------------------------------------------------------------------