├── .gitignore ├── Classes ├── TLKMediaStream.h ├── TLKMediaStream.m ├── TLKSocketIOSignaling.h └── TLKSocketIOSignaling.m ├── LICENSE ├── README.md └── TLKSimpleWebRTC.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | Pods/ 22 | Podfile.lock 23 | -------------------------------------------------------------------------------- /Classes/TLKMediaStream.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLKMediaStreamWrapper.h 3 | // Copyright (c) 2014 &yet, LLC and TLKWebRTC contributors 4 | // 5 | 6 | #import 7 | 8 | @class RTCMediaStream; 9 | 10 | // Simple structure to hold the actual media stream an some useful associated data 11 | // Contents of remoteMediaStreamWrappers in TLKSocketIOSignaling are of this type 12 | @interface TLKMediaStream : NSObject 13 | 14 | @property (nonatomic, readonly) RTCMediaStream *stream; 15 | @property (nonatomic, readonly) NSString *peerID; 16 | @property (nonatomic, readonly) BOOL videoMuted; 17 | @property (nonatomic, readonly) BOOL audioMuted; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Classes/TLKMediaStream.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLKMediaStreamWrapper.m 3 | // Copyright (c) 2014 &yet, LLC and TLKWebRTC contributors 4 | // 5 | 6 | #import "TLKMediaStream.h" 7 | #import "RTCMediaStream.h" 8 | 9 | @interface TLKMediaStream () 10 | { 11 | } 12 | 13 | @property (nonatomic, readwrite) RTCMediaStream *stream; 14 | @property (nonatomic, readwrite) NSString *peerID; 15 | @property (nonatomic, readwrite) BOOL videoMuted; 16 | @property (nonatomic, readwrite) BOOL audioMuted; 17 | 18 | @end 19 | 20 | @implementation TLKMediaStream 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Classes/TLKSocketIOSignaling.h: -------------------------------------------------------------------------------- 1 | // 2 | // TLKSocketIOSignaling.h 3 | // Copyright (c) 2014 &yet, LLC and TLKWebRTC contributors 4 | // 5 | 6 | #import 7 | #import 8 | 9 | typedef void (^TLKSocketIOSignalingSuccessBlock)(void); 10 | typedef void (^TLKSocketIOSignalingFailureBlock)(NSError *error); 11 | 12 | @protocol TLKSocketIOSignalingDelegate; 13 | @class TLKMediaStream; 14 | @class RTCMediaStream; 15 | 16 | @interface TLKSocketIOSignaling : NSObject 17 | 18 | - (instancetype)initWithVideoDevice:(AVCaptureDevice *)device; 19 | - (instancetype)initWithVideo:(BOOL)allowVideo; 20 | 21 | @property (weak, nonatomic) id delegate; 22 | 23 | - (void)connectToServer:(NSString*)apiServer success:(void(^)(void))successCallback failure:(void(^)(NSError*))failureCallback; 24 | - (void)connectToServer:(NSString*)apiServer port:(int)port secure:(BOOL)secure success:(void(^)(void))successCallback failure:(void(^)(NSError*))failureCallback; 25 | - (void)joinRoom:(NSString*)room withKey:(NSString*)key success:(void(^)(void))successCallback failure:(void(^)(void))failureCallback; 26 | - (void)joinRoom:(NSString*)room success:(void(^)(void))successCallback failure:(void(^)(void))failureCallback; 27 | - (void)leaveRoom; 28 | 29 | - (void)lockRoomWithKey:(NSString*)key success:(void(^)(void))successCallback failure:(void(^)(void))failureCallback; 30 | - (void)unlockRoomWithSuccess:(void(^)(void))successCallback failure:(void(^)(void))failureCallback; 31 | 32 | @property (readonly, nonatomic) BOOL allowVideo; 33 | 34 | // Allow the user to see the configured video capture device 35 | @property (readonly) AVCaptureDevice* videoDevice; 36 | 37 | @property (readonly, nonatomic) RTCMediaStream *localMediaStream; 38 | 39 | // each element is a TLKMediaStream, KVO this to get notified when peers connect/disconnect 40 | @property (readonly, nonatomic) NSArray *remoteMediaStreamWrappers; 41 | 42 | // Get / set local audio states. Note: use these instead of setting enabled state directly on localMediaStream, these need to be signaled to keep visuals 43 | // in sync 44 | @property (nonatomic) BOOL localAudioMuted; 45 | @property (nonatomic) BOOL localVideoMuted; 46 | 47 | // Information about the current room state 48 | @property (readonly, nonatomic, getter=isRoomLocked) BOOL roomLocked; 49 | @property (readonly, nonatomic) NSString *roomName; 50 | @property (readonly, nonatomic) NSString *roomKey; 51 | 52 | @end 53 | 54 | @protocol TLKSocketIOSignalingDelegate 55 | @optional 56 | 57 | // Called when a connect request has failed due to a bad room key. Delegate is expected to 58 | // get the room key from the user, and then call connect again with the correct key 59 | - (void)socketIOSignalingRequiresServerPassword:(TLKSocketIOSignaling *)socketIOSignaling; 60 | 61 | - (void)socketIOSignaling:(TLKSocketIOSignaling *)socketIOSignaling addedStream:(TLKMediaStream *)stream; 62 | - (void)socketIOSignaling:(TLKSocketIOSignaling *)socketIOSignaling removedStream:(TLKMediaStream *)stream; 63 | 64 | - (void)socketIOSignaling:(TLKSocketIOSignaling *)socketIOSignaling peer:(NSString *)peer toggledAudioMute:(BOOL)mute; 65 | - (void)socketIOSignaling:(TLKSocketIOSignaling *)socketIOSignaling peer:(NSString *)peer toggledVideoMute:(BOOL)mute; 66 | - (void)socketIOSignaling:(TLKSocketIOSignaling *)socketIOSignaling didChangeLock:(BOOL)locked; 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /Classes/TLKSocketIOSignaling.m: -------------------------------------------------------------------------------- 1 | // 2 | // TLKSocketIOSignaling.m 3 | // Copyright (c) 2014 &yet, LLC and TLKWebRTC contributors 4 | // 5 | 6 | 7 | #import "TLKSocketIOSignaling.h" 8 | #import "TLKMediaStream.h" 9 | 10 | #import "TLKWebRTC.h" 11 | #import "AZSocketIO.h" 12 | #import "RTCMediaStream.h" 13 | #import "RTCICEServer.h" 14 | #import "RTCVideoTrack.h" 15 | #import "RTCAudioTrack.h" 16 | 17 | #define LOG_SIGNALING 1 18 | #ifndef DLog 19 | #if !defined(NDEBUG) && LOG_SIGNALING 20 | # define DLog(fmt, ...) NSLog((@"signaling: " fmt), ##__VA_ARGS__); 21 | #else 22 | # define DLog(...) 23 | #endif 24 | #endif 25 | 26 | #pragma mark - TLKMediaStream 27 | 28 | @interface TLKMediaStream (Secrets) 29 | { 30 | } 31 | 32 | @property (nonatomic, readwrite) RTCMediaStream *stream; 33 | @property (nonatomic, readwrite) NSString *peerID; 34 | @property (nonatomic, readwrite) BOOL videoMuted; 35 | @property (nonatomic, readwrite) BOOL audioMuted; 36 | 37 | @end 38 | 39 | #pragma mark - TLKSocketIOSignaling 40 | 41 | @interface TLKSocketIOSignaling () < 42 | TLKWebRTCDelegate> 43 | { 44 | BOOL _localAudioMuted; 45 | BOOL _localVideoMuted; 46 | } 47 | 48 | @property (nonatomic, strong) AZSocketIO *socket; 49 | @property (nonatomic, strong) TLKWebRTC *webRTC; 50 | 51 | @property (nonatomic, readwrite) NSString *roomName; 52 | @property (nonatomic, readwrite) NSString *roomKey; 53 | 54 | @property (strong, readwrite, nonatomic) RTCMediaStream *localMediaStream; 55 | @property (strong, readwrite, nonatomic) NSArray *remoteMediaStreamWrappers; 56 | 57 | @property (strong, nonatomic) NSMutableSet *currentClients; 58 | 59 | @end 60 | 61 | @implementation TLKSocketIOSignaling 62 | 63 | #pragma mark - getters/setters 64 | 65 | - (BOOL)localAudioMuted { 66 | if (self.localMediaStream.audioTracks.count) { 67 | RTCAudioTrack *audioTrack = self.localMediaStream.audioTracks[0]; 68 | return !audioTrack.isEnabled; 69 | } 70 | return YES; 71 | } 72 | 73 | - (void)setLocalAudioMuted:(BOOL)localAudioMuted { 74 | if(self.localMediaStream.audioTracks.count) { 75 | RTCAudioTrack *audioTrack = self.localMediaStream.audioTracks[0]; 76 | [audioTrack setEnabled:!localAudioMuted]; 77 | [self _sendMuteMessagesForTrack:@"audio" mute:localAudioMuted]; 78 | } 79 | } 80 | 81 | - (BOOL)localVideoMuted { 82 | if (self.localMediaStream.videoTracks.count) { 83 | RTCVideoTrack* videoTrack = self.localMediaStream.videoTracks[0]; 84 | return !videoTrack.isEnabled; 85 | } 86 | return YES; 87 | } 88 | 89 | - (void)setLocalVideoMuted:(BOOL)localVideoMuted { 90 | if (self.localMediaStream.videoTracks.count) { 91 | RTCVideoTrack* videoTrack = self.localMediaStream.videoTracks[0]; 92 | [videoTrack setEnabled:!localVideoMuted]; 93 | [self _sendMuteMessagesForTrack:@"video" mute:localVideoMuted]; 94 | } 95 | } 96 | 97 | - (BOOL)isRoomLocked { 98 | return [self.roomKey length] > 0; 99 | } 100 | 101 | + (NSSet *)keyPathsForValuesAffectingRoomLocked { 102 | return [NSSet setWithObject:@"roomKey"]; 103 | } 104 | 105 | #pragma mark - object lifecycle 106 | 107 | - (instancetype)initWithVideoDevice:(AVCaptureDevice *)device { 108 | self = [super init]; 109 | if (self) { 110 | if (device) { 111 | _allowVideo = YES; 112 | _videoDevice = device; 113 | } 114 | self.currentClients = [[NSMutableSet alloc] init]; 115 | } 116 | return self; 117 | } 118 | 119 | - (instancetype)initWithVideo:(BOOL)allowVideo { 120 | // Set front camera as the default device 121 | AVCaptureDevice* frontCamera; 122 | if (allowVideo) { 123 | frontCamera = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] lastObject]; 124 | } 125 | return [self initWithVideoDevice:frontCamera]; 126 | } 127 | 128 | - (instancetype)init { 129 | // Use default device 130 | return [self initWithVideo:YES]; 131 | } 132 | 133 | #pragma mark - peer/room utilities 134 | 135 | - (void)_disconnectSocket { 136 | [self.socket disconnect]; 137 | self.socket = nil; 138 | } 139 | 140 | - (TLKMediaStream *)_streamForPeerIdentifier:(NSString *)peerIdentifier { 141 | __block TLKMediaStream *found = nil; 142 | 143 | [self.remoteMediaStreamWrappers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 144 | if ([((TLKMediaStream *)obj).peerID isEqualToString:peerIdentifier]) { 145 | found = obj; 146 | *stop = YES; 147 | } 148 | }]; 149 | 150 | return found; 151 | } 152 | 153 | - (void)_peerDisconnectedForIdentifier:(NSString *)peerIdentifier { 154 | NSMutableArray* mutable = [self.remoteMediaStreamWrappers mutableCopy]; 155 | NSMutableIndexSet* toRemove = [NSMutableIndexSet new]; 156 | 157 | [mutable enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 158 | if ([((TLKMediaStream *)obj).peerID isEqualToString:peerIdentifier]) { 159 | [toRemove addIndex:idx]; 160 | } 161 | }]; 162 | 163 | NSArray* objects = [self.remoteMediaStreamWrappers objectsAtIndexes:toRemove]; 164 | 165 | [mutable removeObjectsAtIndexes:toRemove]; 166 | 167 | self.remoteMediaStreamWrappers = mutable; 168 | 169 | if ([self.delegate respondsToSelector:@selector(socketIOSignaling:removedStream:)]) { 170 | for (TLKMediaStream *stream in objects) { 171 | [self.delegate socketIOSignaling:self removedStream:stream]; 172 | } 173 | } 174 | } 175 | 176 | #pragma mark - connect 177 | 178 | - (void)connectToServer:(NSString*)apiServer success:(void(^)(void))successCallback failure:(void(^)(NSError*))failureCallback { 179 | [self connectToServer:apiServer port:8888 secure:YES success:successCallback failure:failureCallback]; 180 | } 181 | 182 | - (void)connectToServer:(NSString*)apiServer port:(int)port secure:(BOOL)secure success:(void(^)(void))successCallback failure:(void(^)(NSError*))failureCallback { 183 | if (self.socket) { 184 | [self _disconnectSocket]; 185 | } 186 | 187 | __weak TLKSocketIOSignaling *weakSelf = self; 188 | 189 | self.socket = [[AZSocketIO alloc] initWithHost:apiServer andPort:[NSString stringWithFormat:@"%d",port] secure:secure]; 190 | 191 | NSString* originURL = [NSString stringWithFormat:@"https://%@:%d", apiServer, port]; 192 | [self.socket setValue:originURL forHTTPHeaderField:@"Origin"]; 193 | 194 | // setup SocketIO blocks 195 | self.socket.messageReceivedBlock = ^(id data) { [weakSelf _socketMessageReceived:data]; }; 196 | self.socket.eventReceivedBlock = ^(NSString *eventName, id data) { [weakSelf _socketEventReceived:eventName withData:data]; }; 197 | self.socket.disconnectedBlock = ^() { [weakSelf _socketDisconnected]; }; 198 | self.socket.errorBlock = ^(NSError *error) { [weakSelf _socketReceivedError:error]; }; 199 | 200 | self.socket.reconnectionLimit = 5.0f; 201 | 202 | if (!self.webRTC) { 203 | if (self.allowVideo && self.videoDevice) { 204 | self.webRTC = [[TLKWebRTC alloc] initWithVideoDevice:self.videoDevice]; 205 | } else { 206 | self.webRTC = [[TLKWebRTC alloc] initWithVideo:NO]; 207 | } 208 | self.webRTC.delegate = self; 209 | } 210 | 211 | [self.socket connectWithSuccess:^{ 212 | dispatch_async(dispatch_get_main_queue(), ^{ 213 | TLKSocketIOSignaling *strongSelf = weakSelf; 214 | strongSelf.localMediaStream = strongSelf.webRTC.localMediaStream; 215 | 216 | if (successCallback) { 217 | successCallback(); 218 | } 219 | }); 220 | } andFailure:^(NSError *error) { 221 | DLog(@"Failed to connect socket.io: %@", error); 222 | if (failureCallback) { 223 | failureCallback(error); 224 | } 225 | }]; 226 | } 227 | 228 | - (void)joinRoom:(NSString*)room withKey:(NSString*)key success:(void(^)(void))successCallback failure:(void(^)(void))failureCallback { 229 | NSError *error = nil; 230 | id args; 231 | if (key) { 232 | args = @{@"name": room, @"key": key}; 233 | } else { 234 | args = room; 235 | } 236 | [self.socket emit:@"join" args:args error:&error ackWithArgs:^(NSArray *data) { 237 | if (data[0] == [NSNull null]) { 238 | NSDictionary* clients = data[1][@"clients"]; 239 | 240 | [[clients allKeys] enumerateObjectsUsingBlock:^(id peerID, NSUInteger idx, BOOL *stop) { 241 | [self.webRTC addPeerConnectionForID:peerID]; 242 | [self.webRTC createOfferForPeerWithID:peerID]; 243 | 244 | [self.currentClients addObject:peerID]; 245 | }]; 246 | 247 | self.roomName = room; 248 | self.roomKey = key; 249 | 250 | if(successCallback) { 251 | successCallback(); 252 | } 253 | } else { 254 | NSLog(@"Error: %@", data[0]); 255 | failureCallback(); 256 | } 257 | }]; 258 | if (error) { 259 | NSLog(@"Error: %@", error); 260 | failureCallback(); 261 | } 262 | } 263 | 264 | - (void)joinRoom:(NSString *)room success:(void(^)(void))successCallback failure:(void(^)(void))failureCallback { 265 | [self joinRoom:room withKey:nil success:successCallback failure:failureCallback]; 266 | } 267 | 268 | - (void)leaveRoom { 269 | [[self.currentClients allObjects] enumerateObjectsUsingBlock:^(id peerID, NSUInteger idx, BOOL *stop) { 270 | [self.webRTC removePeerConnectionForID:peerID]; 271 | [self _peerDisconnectedForIdentifier:peerID]; 272 | }]; 273 | 274 | self.currentClients = [[NSMutableSet alloc] init]; 275 | 276 | [self _disconnectSocket]; 277 | } 278 | 279 | - (void)lockRoomWithKey:(NSString *)key success:(void(^)(void))successCallback failure:(void(^)(void))failureCallback { 280 | NSError *error = nil; 281 | [self.socket emit:@"lockRoom" args:key error:&error ackWithArgs:^(NSArray *data) { 282 | if (data[0] == [NSNull null]) { 283 | if(successCallback) { 284 | successCallback(); 285 | } 286 | } else { 287 | NSLog(@"Error: %@", data[0]); 288 | if(failureCallback) { 289 | failureCallback(); 290 | } 291 | } 292 | }]; 293 | if(error) { 294 | NSLog(@"Error: %@", error); 295 | if(failureCallback) { 296 | failureCallback(); 297 | } 298 | } 299 | } 300 | 301 | - (void)unlockRoomWithSuccess:(void(^)(void))successCallback failure:(void(^)(void))failureCallback { 302 | NSError *error = nil; 303 | [self.socket emit:@"unlockRoom" args:nil error:&error ackWithArgs:^(NSArray *data) { 304 | if (data[0] == [NSNull null]) { 305 | if(successCallback) { 306 | successCallback(); 307 | } 308 | } else { 309 | NSLog(@"Error: %@", data[0]); 310 | if(failureCallback) { 311 | failureCallback(); 312 | } 313 | } 314 | }]; 315 | if (error) { 316 | NSLog(@"Error: %@", error); 317 | if(failureCallback) { 318 | failureCallback(); 319 | } 320 | } 321 | } 322 | 323 | #pragma mark - Mute/Unmute utilities 324 | 325 | - (void)_sendMuteMessagesForTrack:(NSString *)trackString mute:(BOOL)mute { 326 | NSError *error = nil; 327 | 328 | for (NSString* peerID in self.currentClients) { 329 | [self.socket emit:@"message" 330 | args:@{@"to":peerID, 331 | @"type" : mute ? @"mute" : @"unmute", 332 | @"payload": @{@"name":trackString}} 333 | error:&error]; 334 | } 335 | } 336 | 337 | - (void)_broadcastMuteStates { 338 | [self _sendMuteMessagesForTrack:@"audio" mute:self.localAudioMuted]; 339 | [self _sendMuteMessagesForTrack:@"video" mute:self.localVideoMuted]; 340 | } 341 | 342 | #pragma mark - SocketIO methods 343 | 344 | - (void)_socketMessageReceived:(id)data { 345 | } 346 | 347 | - (void)_socketEventReceived:(NSString*)eventName withData:(id)data { 348 | NSDictionary *dictionary = nil; 349 | 350 | if ([eventName isEqualToString:@"locked"]) { 351 | 352 | self.roomKey = (NSString*)[data objectAtIndex:0]; 353 | if ([self.delegate respondsToSelector:@selector(socketIOSignaling:didChangeLock:)]) { 354 | [self.delegate socketIOSignaling:self didChangeLock:YES]; 355 | } 356 | 357 | } else if ([eventName isEqualToString:@"unlocked"]) { 358 | 359 | self.roomKey = nil; 360 | if ([self.delegate respondsToSelector:@selector(socketIOSignaling:didChangeLock:)]) { 361 | [self.delegate socketIOSignaling:self didChangeLock:NO]; 362 | } 363 | 364 | } else if ([eventName isEqualToString:@"passwordRequired"]) { 365 | 366 | if ([self.delegate respondsToSelector:@selector(socketIOSignalingRequiresServerPassword:)]) { 367 | [self.delegate socketIOSignalingRequiresServerPassword:self]; 368 | } 369 | 370 | } else if ([eventName isEqualToString:@"stunservers"] || [eventName isEqualToString:@"turnservers"]) { 371 | 372 | NSArray *serverList = data[0]; 373 | for (NSDictionary *info in serverList) { 374 | NSString *username = info[@"username"] ? info[@"username"] : @""; 375 | NSString *password = info[@"credential"] ? info[@"credential"] : @""; 376 | RTCICEServer *server = [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:info[@"url"]] username:username password:password]; 377 | [self.webRTC addICEServer:server]; 378 | } 379 | 380 | } else { 381 | 382 | dictionary = data[0]; 383 | 384 | if (![dictionary isKindOfClass:[NSDictionary class]]) { 385 | dictionary = nil; 386 | } 387 | 388 | } 389 | 390 | NSLog(@"eventName = %@, type = %@, from = %@, to = %@",eventName, dictionary[@"type"], dictionary[@"from"], dictionary[@"to"]); 391 | 392 | if ([dictionary[@"type"] isEqualToString:@"iceFailed"]) { 393 | 394 | [[[UIAlertView alloc] initWithTitle:@"Connection Failed" message:@"Talky could not establish a connection to a participant in this chat. Please try again later." delegate:nil cancelButtonTitle:@"Continue" otherButtonTitles:nil] show]; 395 | 396 | } else if ([dictionary[@"type"] isEqualToString:@"candidate"]) { 397 | 398 | RTCICECandidate* candidate = [[RTCICECandidate alloc] initWithMid:dictionary[@"payload"][@"candidate"][@"sdpMid"] 399 | index:[dictionary[@"payload"][@"candidate"][@"sdpMLineIndex"] integerValue] 400 | sdp:dictionary[@"payload"][@"candidate"][@"candidate"]]; 401 | 402 | [self.webRTC addICECandidate:candidate forPeerWithID:dictionary[@"from"]]; 403 | 404 | } else if ([dictionary[@"type"] isEqualToString:@"answer"]) { 405 | 406 | RTCSessionDescription* remoteSDP = [[RTCSessionDescription alloc] initWithType:dictionary[@"payload"][@"type"] 407 | sdp:dictionary[@"payload"][@"sdp"]]; 408 | 409 | [self.webRTC setRemoteDescription:remoteSDP forPeerWithID:dictionary[@"from"] receiver:NO]; 410 | 411 | } else if ([dictionary[@"type"] isEqualToString:@"offer"]) { 412 | 413 | [self.webRTC addPeerConnectionForID:dictionary[@"from"]]; 414 | [self.currentClients addObject:dictionary[@"from"]]; 415 | 416 | // Fix for browser-to-app connection crash using beta API. 417 | NSString* origSDP = dictionary[@"payload"][@"sdp"]; 418 | NSError* error; 419 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"m=application \\d+ DTLS/SCTP 5000 *" 420 | options:0 421 | error:&error]; 422 | 423 | NSString* sdp = [regex stringByReplacingMatchesInString:origSDP options:0 range:NSMakeRange(0, [origSDP length]) withTemplate:@"m=application 0 DTLS/SCTP 5000"]; 424 | 425 | RTCSessionDescription* remoteSDP = [[RTCSessionDescription alloc] initWithType:dictionary[@"payload"][@"type"] 426 | sdp:sdp]; 427 | 428 | [self.webRTC setRemoteDescription:remoteSDP forPeerWithID:dictionary[@"from"] receiver:YES]; 429 | 430 | } else if ([eventName isEqualToString:@"remove"]) { 431 | 432 | [self.webRTC removePeerConnectionForID:dictionary[@"id"]]; 433 | [self _peerDisconnectedForIdentifier:dictionary[@"id"]]; 434 | 435 | [self.currentClients removeObject:dictionary[@"id"]]; 436 | 437 | } else if ([dictionary[@"payload"][@"name"] isEqualToString:@"audio"]) { 438 | 439 | TLKMediaStream *stream = [self _streamForPeerIdentifier:dictionary[@"from"]]; 440 | stream.audioMuted = [dictionary[@"type"] isEqualToString:@"mute"]; 441 | if([self.delegate respondsToSelector:@selector(socketIOSignaling:peer:toggledAudioMute:)]) { 442 | [self.delegate socketIOSignaling:self peer:dictionary[@"from"] toggledAudioMute:stream.audioMuted]; 443 | } 444 | 445 | } else if ([dictionary[@"payload"][@"name"] isEqualToString:@"video"]) { 446 | 447 | TLKMediaStream *stream = [self _streamForPeerIdentifier:dictionary[@"from"]]; 448 | stream.videoMuted = [dictionary[@"type"] isEqualToString:@"mute"]; 449 | if([self.delegate respondsToSelector:@selector(socketIOSignaling:peer:toggledVideoMute:)]) { 450 | [self.delegate socketIOSignaling:self peer:dictionary[@"from"] toggledVideoMute:stream.videoMuted]; 451 | } 452 | 453 | } 454 | } 455 | 456 | - (void)_socketDisconnected { 457 | } 458 | 459 | - (void)_socketReceivedError:(NSError *)error { 460 | DLog(@"socket received error occured %@", error); 461 | } 462 | 463 | #pragma mark - TLKWebRTCDelegate 464 | 465 | - (void)webRTC:(TLKWebRTC *)webRTC didSendSDPOffer:(RTCSessionDescription *)offer forPeerWithID:(NSString *)peerID { 466 | NSDictionary *args = @{@"to": peerID, 467 | @"roomType": @"video", 468 | @"type": offer.type, 469 | @"payload": @{@"type": offer.type, @"sdp": offer.description}}; 470 | NSError *error = nil; 471 | [self.socket emit:@"message" args:@[args] error:&error]; 472 | } 473 | 474 | - (void)webRTC:(TLKWebRTC *)webRTC didSendSDPAnswer:(RTCSessionDescription *)answer forPeerWithID:(NSString* )peerID { 475 | NSDictionary *args = @{@"to": peerID, 476 | @"roomType": @"video", 477 | @"type": answer.type, 478 | @"payload": @{@"type": answer.type, @"sdp": answer.description}}; 479 | NSError *error = nil; 480 | [self.socket emit:@"message" args:@[args] error:&error]; 481 | } 482 | 483 | - (void)webRTC:(TLKWebRTC *)webRTC didSendICECandidate:(RTCICECandidate *)candidate forPeerWithID:(NSString *)peerID { 484 | NSDictionary *args = @{@"to": peerID, 485 | @"roomType": @"video", 486 | @"type": @"candidate", 487 | @"payload": @{ @"candidate" : @{@"sdpMid": candidate.sdpMid, 488 | @"sdpMLineIndex": [NSString stringWithFormat:@"%ld", (long)candidate.sdpMLineIndex], 489 | @"candidate": candidate.sdp}}}; 490 | NSError *error = nil; 491 | [self.socket emit:@"message" args:@[args] error:&error]; 492 | } 493 | 494 | - (void)webRTC:(TLKWebRTC *)webRTC didObserveICEConnectionStateChange:(RTCICEConnectionState)state forPeerWithID:(NSString *)peerID { 495 | if ((state == RTCICEConnectionConnected) || (state == RTCICEConnectionClosed)) { 496 | [self _broadcastMuteStates]; 497 | } 498 | else if (state == RTCICEConnectionFailed) { 499 | NSDictionary *args = @{@"to": peerID, 500 | @"type": @"iceFailed"}; 501 | NSError *error = nil; 502 | [self.socket emit:@"message" args:@[args] error:&error]; 503 | [[[UIAlertView alloc] initWithTitle:@"Connection Failed" message:@"Talky could not establish a connection to a participant in this chat. Please try again later." delegate:nil cancelButtonTitle:@"Continue" otherButtonTitles:nil] show]; 504 | } 505 | } 506 | 507 | - (void)webRTC:(TLKWebRTC *)webRTC addedStream:(RTCMediaStream *)stream forPeerWithID:(NSString *)peerID { 508 | TLKMediaStream *tlkStream = [TLKMediaStream new]; 509 | tlkStream.stream = stream; 510 | tlkStream.peerID = peerID; 511 | 512 | if (!self.remoteMediaStreamWrappers) { 513 | self.remoteMediaStreamWrappers = @[tlkStream]; 514 | } 515 | else { 516 | self.remoteMediaStreamWrappers = [self.remoteMediaStreamWrappers arrayByAddingObject:tlkStream]; 517 | } 518 | 519 | if ([self.delegate respondsToSelector:@selector(socketIOSignaling:addedStream:)]) { 520 | [self.delegate socketIOSignaling:self addedStream:tlkStream]; 521 | } 522 | } 523 | 524 | - (void)webRTC:(TLKWebRTC *)webRTC removedStream:(RTCMediaStream *)stream forPeerWithID:(NSString *)peerID { 525 | NSMutableArray *mutable = [self.remoteMediaStreamWrappers mutableCopy]; 526 | NSMutableIndexSet *toRemove = [NSMutableIndexSet new]; 527 | 528 | [mutable enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 529 | if (((TLKMediaStream *)obj).stream == stream) { 530 | [toRemove addIndex:idx]; 531 | } 532 | }]; 533 | 534 | NSArray *objects = [self.remoteMediaStreamWrappers objectsAtIndexes:toRemove]; 535 | 536 | [mutable removeObjectsAtIndexes:toRemove]; 537 | 538 | self.remoteMediaStreamWrappers = mutable; 539 | 540 | if ([self.delegate respondsToSelector:@selector(socketIOSignaling:removedStream:)]) { 541 | for (TLKMediaStream *stream in objects) { 542 | [self.delegate socketIOSignaling:self removedStream:stream]; 543 | } 544 | } 545 | } 546 | 547 | @end 548 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2014 Jon Hjelle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLKSimpleWebRTC 2 | 3 | A iOS interface to connect to WebRTC sessions using a [Signalmaster](https://github.com/andyet/signalmaster) 4 | based signaling server using Socket.io. 5 | 6 | Usage 7 | ----- 8 | 9 | See [otalk/iOS-demo](https://github.com/otalk/iOS-demo) for an example application using the interface. 10 | 11 | **Build Environment** 12 | 13 | We recommend pulling the open source code (TLKSimpleWebRTC, as well as TLKWebRTC - a small part of the project 14 | that is independent of the signaling server) and the precompiled iOS libraries via CocoaPods. 15 | 16 | Here's the Podfile that we use for that: 17 | 18 | target "ios-demo" do 19 | 20 | pod 'libjingle_peerconnection' 21 | pod 'TLKWebRTC', :git => 'https://github.com/otalk/TLKWebRTC.git' 22 | pod 'TLKSimpleWebRTC', :git => 'https://github.com/otalk/TLKSimpleWebRTC.git' 23 | 24 | end 25 | 26 | **Connecting to the signaling server** 27 | 28 | You can connect to the signaling server by allocating a TLKSocketIOSignaling object. You'll also need to set 29 | a delegate to receive messages from the signaling server. 30 | 31 | self.signaling = [[TLKSocketIOSignaling alloc] initWithVideo:YES]; 32 | self.signaling.delegate = self; 33 | 34 | To join a chat, you need to both connect to a server and join a room. 35 | 36 | [self.signaling connectToServer:@"signaling.simplewebrtc.com" port:80 secure:NO success:^{ 37 | [self.signaling joinRoom:@"ios-demo" success:^{ 38 | NSLog(@"join success"); 39 | } failure:^{ 40 | NSLog(@"join failure"); 41 | }]; 42 | NSLog(@"connect success"); 43 | } failure:^(NSError* error) { 44 | NSLog(@"connect failure"); 45 | }]; 46 | 47 | 48 | **Showing the video** 49 | 50 | TLKSimpleWebRTC doesn't provide any interfaces for showing the video, but you can do it with the WebRTC Objective-C 51 | interface (headers included with the precompiled WebRTC libs). Create a RTCEAGLVideoView to render in response to delegate calls from TLKSocketIOSignaling. 52 | 53 | @interface ViewController () 54 | @property (strong, nonatomic) RTCEAGLVideoView* remoteView; 55 | @end 56 | 57 | //... 58 | 59 | - (void)addedStream:(TLKMediaStream *)stream { 60 | if (!self.remoteView) { 61 | self.remoteView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(0, 0, 640, 480)]; 62 | self.remoteView.delegate = self; 63 | [self.view addSubview:self.renderView]; 64 | 65 | [(RTCVideoTrack*)stream.stream.videoTracks[0] addRenderer:self.remoteView]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /TLKSimpleWebRTC.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "TLKSimpleWebRTC" 3 | s.version = "1.0.1" 4 | s.summary = "A iOS interface to a SimpleWebRTC based signalling server using Socket.io" 5 | s.homepage = "https://github.com/otalk/TLKSimpleWebRTC" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Jon Hjelle" => "hjon@andyet.com", 8 | "&yet" => "contact@andyet.com" } 9 | s.platform = :ios, '7.0' 10 | s.source = { :git => "https://github.com/otalk/TLKSimpleWebRTC.git", :tag => s.version.to_s } 11 | s.source_files = "Classes/*.{h,m}" 12 | s.requires_arc = true 13 | s.dependency 'AZSocketIO', '0.0.6' 14 | end 15 | --------------------------------------------------------------------------------