diff --git a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.m b/talk/examples/objc/AppRTCDemo/APPRTCAppClient.m deleted file mode 100644 index 20e708de35..0000000000 --- a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.m +++ /dev/null @@ -1,223 +0,0 @@ -/* - * libjingle - * Copyright 2013, Google Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#if !defined(__has_feature) || !__has_feature(objc_arc) -#error "This file requires ARC support." -#endif - -#import "APPRTCAppClient.h" - -#import - -#import "ARDSignalingParams.h" -#import "ARDUtilities.h" -#import "GAEChannelClient.h" -#import "RTCICEServer.h" -#import "RTCICEServer+JSON.h" -#import "RTCMediaConstraints.h" -#import "RTCPair.h" - -@implementation APPRTCAppClient { - dispatch_queue_t _backgroundQueue; - GAEChannelClient* _gaeChannel; - NSURL* _postMessageURL; - BOOL _verboseLogging; - __weak id _messageHandler; -} - -- (instancetype)initWithDelegate:(id)delegate - messageHandler:(id)handler { - if (self = [super init]) { - _delegate = delegate; - _messageHandler = handler; - _backgroundQueue = dispatch_queue_create("RTCBackgroundQueue", - DISPATCH_QUEUE_SERIAL); - // Uncomment to see Request/Response logging. - // _verboseLogging = YES; - } - return self; -} - -- (void)connectToRoom:(NSURL*)url { - NSString *urlString = - [[url absoluteString] stringByAppendingString:@"&t=json"]; - NSURL *requestURL = [NSURL URLWithString:urlString]; - NSURLRequest *request = [NSURLRequest requestWithURL:requestURL]; - [NSURLConnection sendAsynchronousRequest:request - completionHandler:^(NSURLResponse *response, - NSData *data, - NSError *error) { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - int statusCode = [httpResponse statusCode]; - [self logVerbose:[NSString stringWithFormat: - @"Response received\nURL\n%@\nStatus [%d]\nHeaders\n%@", - [httpResponse URL], - statusCode, - [httpResponse allHeaderFields]]]; - NSAssert(statusCode == 200, - @"Invalid response of %d received while connecting to: %@", - statusCode, - urlString); - if (statusCode != 200) { - return; - } - [self handleResponseData:data forRoomRequest:request]; - }]; -} - -- (void)sendData:(NSData*)data { - NSParameterAssert([data length] > 0); - NSString *message = [NSString stringWithUTF8String:[data bytes]]; - [self logVerbose:[NSString stringWithFormat:@"Send message:\n%@", message]]; - if (!_postMessageURL) { - return; - } - NSMutableURLRequest *request = - [NSMutableURLRequest requestWithURL:_postMessageURL]; - request.HTTPMethod = @"POST"; - [request setHTTPBody:data]; - [NSURLConnection sendAsynchronousRequest:request - completionHandler:^(NSURLResponse *response, - NSData *data, - NSError *error) { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - int status = [httpResponse statusCode]; - NSString *responseString = [data length] > 0 ? - [NSString stringWithUTF8String:[data bytes]] : - nil; - NSAssert(status == 200, - @"Bad response [%d] to message: %@\n\n%@", - status, - message, - responseString); - }]; -} - -#pragma mark - Private - -- (void)logVerbose:(NSString *)message { - if (_verboseLogging) { - NSLog(@"%@", message); - } -} - -- (void)handleResponseData:(NSData *)responseData - forRoomRequest:(NSURLRequest *)request { - ARDSignalingParams *params = - [ARDSignalingParams paramsFromJSONData:responseData]; - if (params.errorMessages.count > 0) { - NSMutableString *message = [NSMutableString string]; - for (NSString *errorMessage in params.errorMessages) { - [message appendFormat:@"%@\n", errorMessage]; - } - [self.delegate appClient:self didErrorWithMessage:message]; - return; - } - [self requestTURNServerForICEServers:params.iceServers - turnServerUrl:[params.turnRequestURL absoluteString]]; - NSString *token = params.channelToken; - [self logVerbose: - [NSString stringWithFormat:@"About to open GAE with token: %@", - token]]; - _gaeChannel = - [[GAEChannelClient alloc] initWithToken:token - delegate:_messageHandler]; - _params = params; - // Generate URL for posting data. - NSDictionary *roomJSON = [NSDictionary dictionaryWithJSONData:responseData]; - _postMessageURL = [self parsePostMessageURLForRoomJSON:roomJSON - request:request]; - [self logVerbose:[NSString stringWithFormat:@"POST message URL:\n%@", - _postMessageURL]]; -} - -- (NSURL*)parsePostMessageURLForRoomJSON:(NSDictionary*)roomJSON - request:(NSURLRequest*)request { - NSString* requestUrl = [[request URL] absoluteString]; - NSRange queryRange = [requestUrl rangeOfString:@"?"]; - NSString* baseUrl = [requestUrl substringToIndex:queryRange.location]; - NSString* roomKey = roomJSON[@"room_key"]; - NSParameterAssert([roomKey length] > 0); - NSString* me = roomJSON[@"me"]; - NSParameterAssert([me length] > 0); - NSString* postMessageUrl = - [NSString stringWithFormat:@"%@/message?r=%@&u=%@", baseUrl, roomKey, me]; - return [NSURL URLWithString:postMessageUrl]; -} - -- (void)requestTURNServerWithUrl:(NSString *)turnServerUrl - completionHandler: - (void (^)(RTCICEServer *turnServer))completionHandler { - NSURL *turnServerURL = [NSURL URLWithString:turnServerUrl]; - NSMutableURLRequest *request = - [NSMutableURLRequest requestWithURL:turnServerURL]; - [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"]; - [request addValue:@"https://apprtc.appspot.com" - forHTTPHeaderField:@"origin"]; - [NSURLConnection sendAsynchronousRequest:request - completionHandler:^(NSURLResponse *response, - NSData *data, - NSError *error) { - if (error) { - NSLog(@"Unable to get TURN server."); - completionHandler(nil); - return; - } - NSDictionary *json = [NSDictionary dictionaryWithJSONData:data]; - RTCICEServer *turnServer = [RTCICEServer serverFromCEODJSONDictionary:json]; - completionHandler(turnServer); - }]; -} - -- (void)requestTURNServerForICEServers:(NSArray*)iceServers - turnServerUrl:(NSString*)turnServerUrl { - BOOL isTurnPresent = NO; - for (RTCICEServer* iceServer in iceServers) { - if ([[iceServer.URI scheme] isEqualToString:@"turn"]) { - isTurnPresent = YES; - break; - } - } - if (!isTurnPresent) { - [self requestTURNServerWithUrl:turnServerUrl - completionHandler:^(RTCICEServer* turnServer) { - NSArray* servers = iceServers; - if (turnServer) { - servers = [servers arrayByAddingObject:turnServer]; - } - NSLog(@"ICE servers:\n%@", servers); - [self.delegate appClient:self didReceiveICEServers:servers]; - }]; - } else { - NSLog(@"ICE servers:\n%@", iceServers); - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate appClient:self didReceiveICEServers:iceServers]; - }); - } -} - -@end diff --git a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m b/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m deleted file mode 100644 index 9134e4dcb9..0000000000 --- a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m +++ /dev/null @@ -1,390 +0,0 @@ -/* - * libjingle - * Copyright 2014, Google Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#import "APPRTCConnectionManager.h" - -#import -#import "APPRTCAppClient.h" -#import "GAEChannelClient.h" -#import "RTCICECandidate.h" -#import "RTCICECandidate+JSON.h" -#import "RTCMediaConstraints.h" -#import "RTCMediaStream.h" -#import "RTCPair.h" -#import "RTCPeerConnection.h" -#import "RTCPeerConnectionDelegate.h" -#import "RTCPeerConnectionFactory.h" -#import "RTCSessionDescription.h" -#import "RTCSessionDescription+JSON.h" -#import "RTCSessionDescriptionDelegate.h" -#import "RTCStatsDelegate.h" -#import "RTCVideoCapturer.h" -#import "RTCVideoSource.h" - -@interface APPRTCConnectionManager () - - -@property(nonatomic, strong) APPRTCAppClient* client; -@property(nonatomic, strong) RTCPeerConnection* peerConnection; -@property(nonatomic, strong) RTCPeerConnectionFactory* peerConnectionFactory; -@property(nonatomic, strong) RTCVideoSource* videoSource; -@property(nonatomic, strong) NSMutableArray* queuedRemoteCandidates; - -@end - -@implementation APPRTCConnectionManager { - NSTimer* _statsTimer; -} - -- (instancetype)initWithDelegate:(id)delegate - logger:(id)logger { - if (self = [super init]) { - self.delegate = delegate; - self.logger = logger; - self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init]; - // TODO(tkchin): turn this into a button. - // Uncomment for stat logs. - // _statsTimer = - // [NSTimer scheduledTimerWithTimeInterval:10 - // target:self - // selector:@selector(didFireStatsTimer:) - // userInfo:nil - // repeats:YES]; - } - return self; -} - -- (void)dealloc { - [self disconnect]; -} - -- (BOOL)connectToRoomWithURL:(NSURL*)url { - if (self.client) { - // Already have a connection. - return NO; - } - self.client = [[APPRTCAppClient alloc] initWithDelegate:self - messageHandler:self]; - [self.client connectToRoom:url]; - return YES; -} - -- (void)disconnect { - if (!self.client) { - return; - } - [self.client - sendData:[@"{\"type\": \"bye\"}" dataUsingEncoding:NSUTF8StringEncoding]]; - [self.peerConnection close]; - self.peerConnection = nil; - self.client = nil; - self.videoSource = nil; - self.queuedRemoteCandidates = nil; -} - -#pragma mark - APPRTCAppClientDelegate - -- (void)appClient:(APPRTCAppClient*)appClient - didErrorWithMessage:(NSString*)message { - [self.delegate connectionManager:self - didErrorWithMessage:message]; -} - -- (void)appClient:(APPRTCAppClient*)appClient - didReceiveICEServers:(NSArray*)servers { - self.queuedRemoteCandidates = [NSMutableArray array]; - RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] - initWithMandatoryConstraints: - @[ - [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"], - [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"] - ] - optionalConstraints: - @[ - [[RTCPair alloc] initWithKey:@"internalSctpDataChannels" - value:@"true"], - [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" - value:@"true"] - ]]; - self.peerConnection = - [self.peerConnectionFactory peerConnectionWithICEServers:servers - constraints:constraints - delegate:self]; - RTCMediaStream* lms = - [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"]; - - // The iOS simulator doesn't provide any sort of camera capture - // support or emulation (http://goo.gl/rHAnC1) so don't bother - // trying to open a local stream. - RTCVideoTrack* localVideoTrack; - - // TODO(tkchin): local video capture for OSX. See - // https://code.google.com/p/webrtc/issues/detail?id=3417. -#if !TARGET_IPHONE_SIMULATOR && TARGET_OS_IPHONE - NSString* cameraID = nil; - for (AVCaptureDevice* captureDevice in - [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { - if (captureDevice.position == AVCaptureDevicePositionFront) { - cameraID = [captureDevice localizedName]; - break; - } - } - NSAssert(cameraID, @"Unable to get the front camera id"); - - RTCVideoCapturer* capturer = - [RTCVideoCapturer capturerWithDeviceName:cameraID]; - self.videoSource = [self.peerConnectionFactory - videoSourceWithCapturer:capturer - constraints:self.client.params.mediaConstraints]; - localVideoTrack = - [self.peerConnectionFactory videoTrackWithID:@"ARDAMSv0" - source:self.videoSource]; - if (localVideoTrack) { - [lms addVideoTrack:localVideoTrack]; - } - [self.delegate connectionManager:self - didReceiveLocalVideoTrack:localVideoTrack]; -#endif - - [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]]; - [self.peerConnection addStream:lms]; - [self.logger logMessage:@"onICEServers - added local stream."]; -} - -#pragma mark - GAEMessageHandler methods - -- (void)onOpen { - if (!self.client.params.isInitiator) { - [self.logger logMessage:@"Callee; waiting for remote offer"]; - return; - } - [self.logger logMessage:@"GAE onOpen - create offer."]; - RTCPair* audio = - [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"]; - RTCPair* video = - [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]; - NSArray* mandatory = @[ audio, video ]; - RTCMediaConstraints* constraints = - [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory - optionalConstraints:nil]; - [self.peerConnection createOfferWithDelegate:self constraints:constraints]; - [self.logger logMessage:@"PC - createOffer."]; -} - -- (void)onMessage:(NSDictionary*)messageData { - NSString* type = messageData[@"type"]; - NSAssert(type, @"Missing type: %@", messageData); - [self.logger logMessage:[NSString stringWithFormat:@"GAE onMessage type - %@", - type]]; - if ([type isEqualToString:@"candidate"]) { - RTCICECandidate* candidate = - [RTCICECandidate candidateFromJSONDictionary:messageData]; - if (self.queuedRemoteCandidates) { - [self.queuedRemoteCandidates addObject:candidate]; - } else { - [self.peerConnection addICECandidate:candidate]; - } - } else if ([type isEqualToString:@"offer"] || - [type isEqualToString:@"answer"]) { - RTCSessionDescription* sdp = - [RTCSessionDescription descriptionFromJSONDictionary:messageData]; - [self.peerConnection setRemoteDescriptionWithDelegate:self - sessionDescription:sdp]; - [self.logger logMessage:@"PC - setRemoteDescription."]; - } else if ([type isEqualToString:@"bye"]) { - [self.delegate connectionManagerDidReceiveHangup:self]; - } else { - NSAssert(NO, @"Invalid message: %@", messageData); - } -} - -- (void)onClose { - [self.logger logMessage:@"GAE onClose."]; - [self.delegate connectionManagerDidReceiveHangup:self]; -} - -- (void)onError:(int)code withDescription:(NSString*)description { - NSString* message = [NSString stringWithFormat:@"GAE onError: %d, %@", - code, description]; - [self.logger logMessage:message]; - [self.delegate connectionManager:self - didErrorWithMessage:message]; -} - -#pragma mark - RTCPeerConnectionDelegate - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - signalingStateChanged:(RTCSignalingState)stateChanged { - dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"PCO onSignalingStateChange: %d", stateChanged); - }); -} - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - addedStream:(RTCMediaStream*)stream { - dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"PCO onAddStream."); - NSAssert([stream.audioTracks count] == 1 || [stream.videoTracks count] == 1, - @"Expected audio or video track"); - NSAssert([stream.audioTracks count] <= 1, - @"Expected at most 1 audio stream"); - NSAssert([stream.videoTracks count] <= 1, - @"Expected at most 1 video stream"); - if ([stream.videoTracks count] != 0) { - [self.delegate connectionManager:self - didReceiveRemoteVideoTrack:stream.videoTracks[0]]; - } - }); -} - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - removedStream:(RTCMediaStream*)stream { - dispatch_async(dispatch_get_main_queue(), - ^{ NSLog(@"PCO onRemoveStream."); }); -} - -- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection*)peerConnection { - dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"PCO onRenegotiationNeeded - ignoring because AppRTC has a " - "predefined negotiation strategy"); - }); -} - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - gotICECandidate:(RTCICECandidate*)candidate { - dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"PCO onICECandidate.\n%@", candidate); - [self.client sendData:[candidate JSONData]]; - }); -} - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - iceGatheringChanged:(RTCICEGatheringState)newState { - dispatch_async(dispatch_get_main_queue(), - ^{ NSLog(@"PCO onIceGatheringChange. %d", newState); }); -} - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - iceConnectionChanged:(RTCICEConnectionState)newState { - dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"PCO onIceConnectionChange. %d", newState); - if (newState == RTCICEConnectionConnected) - [self.logger logMessage:@"ICE Connection Connected."]; - NSAssert(newState != RTCICEConnectionFailed, @"ICE Connection failed!"); - }); -} - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - didOpenDataChannel:(RTCDataChannel*)dataChannel { - NSAssert(NO, @"AppRTC doesn't use DataChannels"); -} - -#pragma mark - RTCSessionDescriptionDelegate - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - didCreateSessionDescription:(RTCSessionDescription*)sdp - error:(NSError*)error { - dispatch_async(dispatch_get_main_queue(), ^{ - if (error) { - [self.logger logMessage:@"SDP onFailure."]; - NSAssert(NO, error.description); - return; - } - [self.logger logMessage:@"SDP onSuccess(SDP) - set local description."]; - [self.peerConnection setLocalDescriptionWithDelegate:self - sessionDescription:sdp]; - [self.logger logMessage:@"PC setLocalDescription."]; - [self.client sendData:[sdp JSONData]]; - }); -} - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - didSetSessionDescriptionWithError:(NSError*)error { - dispatch_async(dispatch_get_main_queue(), ^{ - if (error) { - [self.logger logMessage:@"SDP onFailure."]; - NSAssert(NO, error.description); - return; - } - [self.logger logMessage:@"SDP onSuccess() - possibly drain candidates"]; - if (!self.client.params.isInitiator) { - if (self.peerConnection.remoteDescription && - !self.peerConnection.localDescription) { - [self.logger logMessage:@"Callee, setRemoteDescription succeeded"]; - RTCPair* audio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" - value:@"true"]; - RTCPair* video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" - value:@"true"]; - NSArray* mandatory = @[ audio, video ]; - RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] - initWithMandatoryConstraints:mandatory - optionalConstraints:nil]; - [self.peerConnection createAnswerWithDelegate:self - constraints:constraints]; - [self.logger logMessage:@"PC - createAnswer."]; - } else { - [self.logger logMessage:@"SDP onSuccess - drain candidates"]; - [self drainRemoteCandidates]; - } - } else { - if (self.peerConnection.remoteDescription) { - [self.logger logMessage:@"SDP onSuccess - drain candidates"]; - [self drainRemoteCandidates]; - } - } - }); -} - -#pragma mark - RTCStatsDelegate methods - -- (void)peerConnection:(RTCPeerConnection*)peerConnection - didGetStats:(NSArray*)stats { - dispatch_async(dispatch_get_main_queue(), ^{ - NSString* message = [NSString stringWithFormat:@"Stats:\n %@", stats]; - [self.logger logMessage:message]; - }); -} - -#pragma mark - Private - -- (void)drainRemoteCandidates { - for (RTCICECandidate* candidate in self.queuedRemoteCandidates) { - [self.peerConnection addICECandidate:candidate]; - } - self.queuedRemoteCandidates = nil; -} - -- (void)didFireStatsTimer:(NSTimer*)timer { - if (self.peerConnection) { - [self.peerConnection getStatsWithDelegate:self - mediaStreamTrack:nil - statsOutputLevel:RTCStatsOutputLevelDebug]; - } -} - -@end diff --git a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.h b/talk/examples/objc/AppRTCDemo/ARDAppClient.h similarity index 50% rename from talk/examples/objc/AppRTCDemo/APPRTCAppClient.h rename to talk/examples/objc/AppRTCDemo/ARDAppClient.h index 880d5f8a48..d742ea3f9a 100644 --- a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.h +++ b/talk/examples/objc/AppRTCDemo/ARDAppClient.h @@ -1,6 +1,6 @@ /* * libjingle - * Copyright 2013, Google Inc. + * Copyright 2014, Google Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -27,42 +27,50 @@ #import -#import "ARDSignalingParams.h" -#import "GAEChannelClient.h" +#import "RTCVideoTrack.h" -@class APPRTCAppClient; -@protocol APPRTCAppClientDelegate +typedef NS_ENUM(NSInteger, ARDAppClientState) { + // Disconnected from servers. + kARDAppClientStateDisconnected, + // Connecting to servers. + kARDAppClientStateConnecting, + // Connected to servers. + kARDAppClientStateConnected, +}; -- (void)appClient:(APPRTCAppClient*)appClient - didErrorWithMessage:(NSString*)message; -- (void)appClient:(APPRTCAppClient*)appClient - didReceiveICEServers:(NSArray*)servers; +@class ARDAppClient; +@protocol ARDAppClientDelegate + +- (void)appClient:(ARDAppClient *)client + didChangeState:(ARDAppClientState)state; + +- (void)appClient:(ARDAppClient *)client + didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack; + +- (void)appClient:(ARDAppClient *)client + didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack; + +- (void)appClient:(ARDAppClient *)client + didError:(NSError *)error; @end -@class RTCMediaConstraints; +// Handles connections to the AppRTC server for a given room. +@interface ARDAppClient : NSObject -// Negotiates signaling for chatting with apprtc.appspot.com "rooms". -// Uses the client<->server specifics of the apprtc AppEngine webapp. -// -// To use: create an instance of this object (registering a message handler) and -// call connectToRoom(). apprtc.appspot.com will signal that is successful via -// onOpen through the browser channel. Then you should call sendData() and wait -// for the registered handler to be called with received messages. -@interface APPRTCAppClient : NSObject +@property(nonatomic, readonly) ARDAppClientState state; +@property(nonatomic, weak) id delegate; -@property(nonatomic, readonly) ARDSignalingParams *params; -@property(nonatomic, weak) id delegate; +- (instancetype)initWithDelegate:(id)delegate; -- (instancetype)initWithDelegate:(id)delegate - messageHandler:(id)handler; -- (void)connectToRoom:(NSURL *)room; -- (void)sendData:(NSData *)data; +// Establishes a connection with the AppRTC servers for the given room id. +// TODO(tkchin): provide available keys/values for options. This will be used +// for call configurations such as overriding server choice, specifying codecs +// and so on. +- (void)connectToRoomWithId:(NSString *)roomId + options:(NSDictionary *)options; -#ifndef DOXYGEN_SHOULD_SKIP_THIS -// Disallow init and don't add to documentation -- (instancetype)init __attribute__(( - unavailable("init is not a supported initializer for this class."))); -#endif /* DOXYGEN_SHOULD_SKIP_THIS */ +// Disconnects from the AppRTC servers and any connected clients. +- (void)disconnect; @end diff --git a/talk/examples/objc/AppRTCDemo/ARDAppClient.m b/talk/examples/objc/AppRTCDemo/ARDAppClient.m new file mode 100644 index 0000000000..eba3ae9db8 --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/ARDAppClient.m @@ -0,0 +1,675 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "ARDAppClient.h" + +#import + +#import "ARDMessageResponse.h" +#import "ARDRegisterResponse.h" +#import "ARDSignalingMessage.h" +#import "ARDUtilities.h" +#import "ARDWebSocketChannel.h" +#import "RTCICECandidate+JSON.h" +#import "RTCICEServer+JSON.h" +#import "RTCMediaConstraints.h" +#import "RTCMediaStream.h" +#import "RTCPair.h" +#import "RTCPeerConnection.h" +#import "RTCPeerConnectionDelegate.h" +#import "RTCPeerConnectionFactory.h" +#import "RTCSessionDescription+JSON.h" +#import "RTCSessionDescriptionDelegate.h" +#import "RTCVideoCapturer.h" +#import "RTCVideoTrack.h" + +// TODO(tkchin): move these to a configuration object. +static NSString *kARDRoomServerHostUrl = + @"https://3-dot-apprtc.appspot.com"; +static NSString *kARDRoomServerRegisterFormat = + @"https://3-dot-apprtc.appspot.com/register/%@"; +static NSString *kARDRoomServerMessageFormat = + @"https://3-dot-apprtc.appspot.com/message/%@/%@"; +static NSString *kARDRoomServerByeFormat = + @"https://3-dot-apprtc.appspot.com/bye/%@/%@"; + +static NSString *kARDDefaultSTUNServerUrl = + @"stun:stun.l.google.com:19302"; +// TODO(tkchin): figure out a better username for CEOD statistics. +static NSString *kARDTurnRequestUrl = + @"https://computeengineondemand.appspot.com" + @"/turn?username=iapprtc&key=4080218913"; + +static NSString *kARDAppClientErrorDomain = @"ARDAppClient"; +static NSInteger kARDAppClientErrorUnknown = -1; +static NSInteger kARDAppClientErrorRoomFull = -2; +static NSInteger kARDAppClientErrorCreateSDP = -3; +static NSInteger kARDAppClientErrorSetSDP = -4; +static NSInteger kARDAppClientErrorNetwork = -5; +static NSInteger kARDAppClientErrorInvalidClient = -6; +static NSInteger kARDAppClientErrorInvalidRoom = -7; + +@interface ARDAppClient () +@property(nonatomic, strong) ARDWebSocketChannel *channel; +@property(nonatomic, strong) RTCPeerConnection *peerConnection; +@property(nonatomic, strong) RTCPeerConnectionFactory *factory; +@property(nonatomic, strong) NSMutableArray *messageQueue; + +@property(nonatomic, assign) BOOL isTurnComplete; +@property(nonatomic, assign) BOOL hasReceivedSdp; +@property(nonatomic, readonly) BOOL isRegisteredWithRoomServer; + +@property(nonatomic, strong) NSString *roomId; +@property(nonatomic, strong) NSString *clientId; +@property(nonatomic, assign) BOOL isInitiator; +@property(nonatomic, strong) NSMutableArray *iceServers; +@property(nonatomic, strong) NSURL *webSocketURL; +@property(nonatomic, strong) NSURL *webSocketRestURL; +@end + +@implementation ARDAppClient + +@synthesize delegate = _delegate; +@synthesize state = _state; +@synthesize channel = _channel; +@synthesize peerConnection = _peerConnection; +@synthesize factory = _factory; +@synthesize messageQueue = _messageQueue; +@synthesize isTurnComplete = _isTurnComplete; +@synthesize hasReceivedSdp = _hasReceivedSdp; +@synthesize roomId = _roomId; +@synthesize clientId = _clientId; +@synthesize isInitiator = _isInitiator; +@synthesize iceServers = _iceServers; +@synthesize webSocketURL = _websocketURL; +@synthesize webSocketRestURL = _websocketRestURL; + +- (instancetype)initWithDelegate:(id)delegate { + if (self = [super init]) { + _delegate = delegate; + _factory = [[RTCPeerConnectionFactory alloc] init]; + _messageQueue = [NSMutableArray array]; + _iceServers = [NSMutableArray arrayWithObject:[self defaultSTUNServer]]; + } + return self; +} + +- (void)dealloc { + [self disconnect]; +} + +- (void)setState:(ARDAppClientState)state { + if (_state == state) { + return; + } + _state = state; + [_delegate appClient:self didChangeState:_state]; +} + +- (void)connectToRoomWithId:(NSString *)roomId + options:(NSDictionary *)options { + NSParameterAssert(roomId.length); + NSParameterAssert(_state == kARDAppClientStateDisconnected); + self.state = kARDAppClientStateConnecting; + + // Request TURN. + __weak ARDAppClient *weakSelf = self; + NSURL *turnRequestURL = [NSURL URLWithString:kARDTurnRequestUrl]; + [self requestTURNServersWithURL:turnRequestURL + completionHandler:^(NSArray *turnServers) { + ARDAppClient *strongSelf = weakSelf; + [strongSelf.iceServers addObjectsFromArray:turnServers]; + strongSelf.isTurnComplete = YES; + [strongSelf startSignalingIfReady]; + }]; + + // Register with room server. + [self registerWithRoomServerForRoomId:roomId + completionHandler:^(ARDRegisterResponse *response) { + ARDAppClient *strongSelf = weakSelf; + if (!response || response.result != kARDRegisterResultTypeSuccess) { + NSLog(@"Failed to register with room server. Result:%d", + (int)response.result); + [strongSelf disconnect]; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: @"Room is full.", + }; + NSError *error = + [[NSError alloc] initWithDomain:kARDAppClientErrorDomain + code:kARDAppClientErrorRoomFull + userInfo:userInfo]; + [strongSelf.delegate appClient:strongSelf didError:error]; + return; + } + NSLog(@"Registered with room server."); + strongSelf.roomId = response.roomId; + strongSelf.clientId = response.clientId; + strongSelf.isInitiator = response.isInitiator; + for (ARDSignalingMessage *message in response.messages) { + if (message.type == kARDSignalingMessageTypeOffer || + message.type == kARDSignalingMessageTypeAnswer) { + strongSelf.hasReceivedSdp = YES; + [strongSelf.messageQueue insertObject:message atIndex:0]; + } else { + [strongSelf.messageQueue addObject:message]; + } + } + strongSelf.webSocketURL = response.webSocketURL; + strongSelf.webSocketRestURL = response.webSocketRestURL; + [strongSelf registerWithColliderIfReady]; + [strongSelf startSignalingIfReady]; + }]; +} + +- (void)disconnect { + if (_state == kARDAppClientStateDisconnected) { + return; + } + if (self.isRegisteredWithRoomServer) { + [self unregisterWithRoomServer]; + } + if (_channel) { + if (_channel.state == kARDWebSocketChannelStateRegistered) { + // Tell the other client we're hanging up. + ARDByeMessage *byeMessage = [[ARDByeMessage alloc] init]; + NSData *byeData = [byeMessage JSONData]; + [_channel sendData:byeData]; + } + // Disconnect from collider. + _channel = nil; + } + _clientId = nil; + _roomId = nil; + _isInitiator = NO; + _hasReceivedSdp = NO; + _messageQueue = [NSMutableArray array]; + _peerConnection = nil; + self.state = kARDAppClientStateDisconnected; +} + +#pragma mark - ARDWebSocketChannelDelegate + +- (void)channel:(ARDWebSocketChannel *)channel + didReceiveMessage:(ARDSignalingMessage *)message { + switch (message.type) { + case kARDSignalingMessageTypeOffer: + case kARDSignalingMessageTypeAnswer: + _hasReceivedSdp = YES; + [_messageQueue insertObject:message atIndex:0]; + break; + case kARDSignalingMessageTypeCandidate: + [_messageQueue addObject:message]; + break; + case kARDSignalingMessageTypeBye: + [self processSignalingMessage:message]; + return; + } + [self drainMessageQueueIfReady]; +} + +- (void)channel:(ARDWebSocketChannel *)channel + didChangeState:(ARDWebSocketChannelState)state { + switch (state) { + case kARDWebSocketChannelStateOpen: + break; + case kARDWebSocketChannelStateRegistered: + break; + case kARDWebSocketChannelStateClosed: + case kARDWebSocketChannelStateError: + // TODO(tkchin): reconnection scenarios. Right now we just disconnect + // completely if the websocket connection fails. + [self disconnect]; + break; + } +} + +#pragma mark - RTCPeerConnectionDelegate + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + signalingStateChanged:(RTCSignalingState)stateChanged { + NSLog(@"Signaling state changed: %d", stateChanged); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + addedStream:(RTCMediaStream *)stream { + dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"Received %lu video tracks and %lu audio tracks", + (unsigned long)stream.videoTracks.count, + (unsigned long)stream.audioTracks.count); + if (stream.videoTracks.count) { + RTCVideoTrack *videoTrack = stream.videoTracks[0]; + [_delegate appClient:self didReceiveRemoteVideoTrack:videoTrack]; + } + }); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + removedStream:(RTCMediaStream *)stream { + NSLog(@"Stream was removed."); +} + +- (void)peerConnectionOnRenegotiationNeeded: + (RTCPeerConnection *)peerConnection { + NSLog(@"WARNING: Renegotiation needed but unimplemented."); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + iceConnectionChanged:(RTCICEConnectionState)newState { + NSLog(@"ICE state changed: %d", newState); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + iceGatheringChanged:(RTCICEGatheringState)newState { + NSLog(@"ICE gathering state changed: %d", newState); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + gotICECandidate:(RTCICECandidate *)candidate { + dispatch_async(dispatch_get_main_queue(), ^{ + ARDICECandidateMessage *message = + [[ARDICECandidateMessage alloc] initWithCandidate:candidate]; + [self sendSignalingMessage:message]; + }); +} + +- (void)peerConnection:(RTCPeerConnection*)peerConnection + didOpenDataChannel:(RTCDataChannel*)dataChannel { +} + +#pragma mark - RTCSessionDescriptionDelegate + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didCreateSessionDescription:(RTCSessionDescription *)sdp + error:(NSError *)error { + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + NSLog(@"Failed to create session description. Error: %@", error); + [self disconnect]; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: @"Failed to create session description.", + }; + NSError *sdpError = + [[NSError alloc] initWithDomain:kARDAppClientErrorDomain + code:kARDAppClientErrorCreateSDP + userInfo:userInfo]; + [_delegate appClient:self didError:sdpError]; + return; + } + [_peerConnection setLocalDescriptionWithDelegate:self + sessionDescription:sdp]; + ARDSessionDescriptionMessage *message = + [[ARDSessionDescriptionMessage alloc] initWithDescription:sdp]; + [self sendSignalingMessage:message]; + }); +} + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didSetSessionDescriptionWithError:(NSError *)error { + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + NSLog(@"Failed to set session description. Error: %@", error); + [self disconnect]; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: @"Failed to set session description.", + }; + NSError *sdpError = + [[NSError alloc] initWithDomain:kARDAppClientErrorDomain + code:kARDAppClientErrorSetSDP + userInfo:userInfo]; + [_delegate appClient:self didError:sdpError]; + return; + } + // If we're answering and we've just set the remote offer we need to create + // an answer and set the local description. + if (!_isInitiator && !_peerConnection.localDescription) { + RTCMediaConstraints *constraints = [self defaultAnswerConstraints]; + [_peerConnection createAnswerWithDelegate:self + constraints:constraints]; + + } + }); +} + +#pragma mark - Private + +- (BOOL)isRegisteredWithRoomServer { + return _clientId.length; +} + +- (void)startSignalingIfReady { + if (!_isTurnComplete || !self.isRegisteredWithRoomServer) { + return; + } + self.state = kARDAppClientStateConnected; + + // Create peer connection. + RTCMediaConstraints *constraints = [self defaultPeerConnectionConstraints]; + _peerConnection = [_factory peerConnectionWithICEServers:_iceServers + constraints:constraints + delegate:self]; + RTCMediaStream *localStream = [self createLocalMediaStream]; + [_peerConnection addStream:localStream]; + if (_isInitiator) { + [self sendOffer]; + } else { + [self waitForAnswer]; + } +} + +- (void)sendOffer { + [_peerConnection createOfferWithDelegate:self + constraints:[self defaultOfferConstraints]]; +} + +- (void)waitForAnswer { + [self drainMessageQueueIfReady]; +} + +- (void)drainMessageQueueIfReady { + if (!_peerConnection || !_hasReceivedSdp) { + return; + } + for (ARDSignalingMessage *message in _messageQueue) { + [self processSignalingMessage:message]; + } + [_messageQueue removeAllObjects]; +} + +- (void)processSignalingMessage:(ARDSignalingMessage *)message { + NSParameterAssert(_peerConnection || + message.type == kARDSignalingMessageTypeBye); + switch (message.type) { + case kARDSignalingMessageTypeOffer: + case kARDSignalingMessageTypeAnswer: { + ARDSessionDescriptionMessage *sdpMessage = + (ARDSessionDescriptionMessage *)message; + RTCSessionDescription *description = sdpMessage.sessionDescription; + [_peerConnection setRemoteDescriptionWithDelegate:self + sessionDescription:description]; + break; + } + case kARDSignalingMessageTypeCandidate: { + ARDICECandidateMessage *candidateMessage = + (ARDICECandidateMessage *)message; + [_peerConnection addICECandidate:candidateMessage.candidate]; + break; + } + case kARDSignalingMessageTypeBye: + // Other client disconnected. + // TODO(tkchin): support waiting in room for next client. For now just + // disconnect. + [self disconnect]; + break; + } +} + +- (void)sendSignalingMessage:(ARDSignalingMessage *)message { + if (_isInitiator) { + [self sendSignalingMessageToRoomServer:message completionHandler:nil]; + } else { + [self sendSignalingMessageToCollider:message]; + } +} + +- (RTCMediaStream *)createLocalMediaStream { + RTCMediaStream* localStream = [_factory mediaStreamWithLabel:@"ARDAMS"]; + RTCVideoTrack* localVideoTrack = nil; + + // The iOS simulator doesn't provide any sort of camera capture + // support or emulation (http://goo.gl/rHAnC1) so don't bother + // trying to open a local stream. + // TODO(tkchin): local video capture for OSX. See + // https://code.google.com/p/webrtc/issues/detail?id=3417. +#if !TARGET_IPHONE_SIMULATOR && TARGET_OS_IPHONE + NSString *cameraID = nil; + for (AVCaptureDevice *captureDevice in + [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { + if (captureDevice.position == AVCaptureDevicePositionFront) { + cameraID = [captureDevice localizedName]; + break; + } + } + NSAssert(cameraID, @"Unable to get the front camera id"); + + RTCVideoCapturer *capturer = + [RTCVideoCapturer capturerWithDeviceName:cameraID]; + RTCMediaConstraints *mediaConstraints = [self defaultMediaStreamConstraints]; + RTCVideoSource *videoSource = + [_factory videoSourceWithCapturer:capturer + constraints:mediaConstraints]; + localVideoTrack = + [_factory videoTrackWithID:@"ARDAMSv0" source:videoSource]; + if (localVideoTrack) { + [localStream addVideoTrack:localVideoTrack]; + } + [_delegate appClient:self didReceiveLocalVideoTrack:localVideoTrack]; +#endif + [localStream addAudioTrack:[_factory audioTrackWithID:@"ARDAMSa0"]]; + return localStream; +} + +- (void)requestTURNServersWithURL:(NSURL *)requestURL + completionHandler:(void (^)(NSArray *turnServers))completionHandler { + NSParameterAssert([requestURL absoluteString].length); + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:requestURL]; + // We need to set origin because TURN provider whitelists requests based on + // origin. + [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"]; + [request addValue:kARDRoomServerHostUrl forHTTPHeaderField:@"origin"]; + [NSURLConnection sendAsyncRequest:request + completionHandler:^(NSURLResponse *response, + NSData *data, + NSError *error) { + NSArray *turnServers = [NSArray array]; + if (error) { + NSLog(@"Unable to get TURN server."); + completionHandler(turnServers); + return; + } + NSDictionary *dict = [NSDictionary dictionaryWithJSONData:data]; + turnServers = [RTCICEServer serversFromCEODJSONDictionary:dict]; + completionHandler(turnServers); + }]; +} + +#pragma mark - Room server methods + +- (void)registerWithRoomServerForRoomId:(NSString *)roomId + completionHandler:(void (^)(ARDRegisterResponse *))completionHandler { + NSString *urlString = + [NSString stringWithFormat:kARDRoomServerRegisterFormat, roomId]; + NSURL *roomURL = [NSURL URLWithString:urlString]; + NSLog(@"Registering with room server."); + __weak ARDAppClient *weakSelf = self; + [NSURLConnection sendAsyncPostToURL:roomURL + withData:nil + completionHandler:^(BOOL succeeded, NSData *data) { + ARDAppClient *strongSelf = weakSelf; + if (!succeeded) { + NSError *error = [self roomServerNetworkError]; + [strongSelf.delegate appClient:strongSelf didError:error]; + completionHandler(nil); + return; + } + ARDRegisterResponse *response = + [ARDRegisterResponse responseFromJSONData:data]; + completionHandler(response); + }]; +} + +- (void)sendSignalingMessageToRoomServer:(ARDSignalingMessage *)message + completionHandler:(void (^)(ARDMessageResponse *))completionHandler { + NSData *data = [message JSONData]; + NSString *urlString = + [NSString stringWithFormat: + kARDRoomServerMessageFormat, _roomId, _clientId]; + NSURL *url = [NSURL URLWithString:urlString]; + NSLog(@"C->RS POST: %@", message); + __weak ARDAppClient *weakSelf = self; + [NSURLConnection sendAsyncPostToURL:url + withData:data + completionHandler:^(BOOL succeeded, NSData *data) { + ARDAppClient *strongSelf = weakSelf; + if (!succeeded) { + NSError *error = [self roomServerNetworkError]; + [strongSelf.delegate appClient:strongSelf didError:error]; + return; + } + ARDMessageResponse *response = + [ARDMessageResponse responseFromJSONData:data]; + NSError *error = nil; + switch (response.result) { + case kARDMessageResultTypeSuccess: + break; + case kARDMessageResultTypeUnknown: + error = + [[NSError alloc] initWithDomain:kARDAppClientErrorDomain + code:kARDAppClientErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey: @"Unknown error.", + }]; + case kARDMessageResultTypeInvalidClient: + error = + [[NSError alloc] initWithDomain:kARDAppClientErrorDomain + code:kARDAppClientErrorInvalidClient + userInfo:@{ + NSLocalizedDescriptionKey: @"Invalid client.", + }]; + break; + case kARDMessageResultTypeInvalidRoom: + error = + [[NSError alloc] initWithDomain:kARDAppClientErrorDomain + code:kARDAppClientErrorInvalidRoom + userInfo:@{ + NSLocalizedDescriptionKey: @"Invalid room.", + }]; + break; + }; + if (error) { + [strongSelf.delegate appClient:strongSelf didError:error]; + } + if (completionHandler) { + completionHandler(response); + } + }]; +} + +- (void)unregisterWithRoomServer { + NSString *urlString = + [NSString stringWithFormat:kARDRoomServerByeFormat, _roomId, _clientId]; + NSURL *url = [NSURL URLWithString:urlString]; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + NSURLResponse *response = nil; + // We want a synchronous request so that we know that we're unregistered from + // room server before we do any further unregistration. + NSLog(@"C->RS: BYE"); + NSError *error = nil; + [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&error]; + if (error) { + NSLog(@"Error unregistering from room server: %@", error); + } + NSLog(@"Unregistered from room server."); +} + +- (NSError *)roomServerNetworkError { + NSError *error = + [[NSError alloc] initWithDomain:kARDAppClientErrorDomain + code:kARDAppClientErrorNetwork + userInfo:@{ + NSLocalizedDescriptionKey: @"Room server network error", + }]; + return error; +} + +#pragma mark - Collider methods + +- (void)registerWithColliderIfReady { + if (!self.isRegisteredWithRoomServer) { + return; + } + // Open WebSocket connection. + _channel = + [[ARDWebSocketChannel alloc] initWithURL:_websocketURL + restURL:_websocketRestURL + delegate:self]; + [_channel registerForRoomId:_roomId clientId:_clientId]; +} + +- (void)sendSignalingMessageToCollider:(ARDSignalingMessage *)message { + NSData *data = [message JSONData]; + [_channel sendData:data]; +} + +#pragma mark - Defaults + +- (RTCMediaConstraints *)defaultMediaStreamConstraints { + RTCMediaConstraints* constraints = + [[RTCMediaConstraints alloc] + initWithMandatoryConstraints:nil + optionalConstraints:nil]; + return constraints; +} + +- (RTCMediaConstraints *)defaultAnswerConstraints { + return [self defaultOfferConstraints]; +} + +- (RTCMediaConstraints *)defaultOfferConstraints { + NSArray *mandatoryConstraints = @[ + [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"], + [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"] + ]; + RTCMediaConstraints* constraints = + [[RTCMediaConstraints alloc] + initWithMandatoryConstraints:mandatoryConstraints + optionalConstraints:nil]; + return constraints; +} + +- (RTCMediaConstraints *)defaultPeerConnectionConstraints { + NSArray *optionalConstraints = @[ + [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"] + ]; + RTCMediaConstraints* constraints = + [[RTCMediaConstraints alloc] + initWithMandatoryConstraints:nil + optionalConstraints:optionalConstraints]; + return constraints; +} + +- (RTCICEServer *)defaultSTUNServer { + NSURL *defaultSTUNServerURL = [NSURL URLWithString:kARDDefaultSTUNServerUrl]; + return [[RTCICEServer alloc] initWithURI:defaultSTUNServerURL + username:@"" + password:@""]; +} + +@end diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.h similarity index 69% rename from talk/examples/objc/AppRTCDemo/ARDSignalingParams.h rename to talk/examples/objc/AppRTCDemo/ARDMessageResponse.h index df2114c384..bbe35ab1a7 100644 --- a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h +++ b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.h @@ -27,20 +27,17 @@ #import -#import "RTCMediaConstraints.h" +typedef NS_ENUM(NSInteger, ARDMessageResultType) { + kARDMessageResultTypeUnknown, + kARDMessageResultTypeSuccess, + kARDMessageResultTypeInvalidRoom, + kARDMessageResultTypeInvalidClient +}; -// Struct for holding the signaling parameters of an AppRTC room. -@interface ARDSignalingParams : NSObject +@interface ARDMessageResponse : NSObject -@property(nonatomic, assign) BOOL isInitiator; -@property(nonatomic, readonly) NSArray *errorMessages; -@property(nonatomic, readonly) RTCMediaConstraints *offerConstraints; -@property(nonatomic, readonly) RTCMediaConstraints *mediaConstraints; -@property(nonatomic, readonly) NSMutableArray *iceServers; -@property(nonatomic, readonly) NSURL *signalingServerURL; -@property(nonatomic, readonly) NSURL *turnRequestURL; -@property(nonatomic, readonly) NSString *channelToken; +@property(nonatomic, readonly) ARDMessageResultType result; -+ (ARDSignalingParams *)paramsFromJSONData:(NSData *)data; ++ (ARDMessageResponse *)responseFromJSONData:(NSData *)data; @end diff --git a/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m new file mode 100644 index 0000000000..c6ab1d4414 --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m @@ -0,0 +1,69 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "ARDMessageResponse.h" + +#import "ARDUtilities.h" + +static NSString const *kARDMessageResultKey = @"result"; + +@interface ARDMessageResponse () + +@property(nonatomic, assign) ARDMessageResultType result; + +@end + +@implementation ARDMessageResponse + +@synthesize result = _result; + ++ (ARDMessageResponse *)responseFromJSONData:(NSData *)data { + NSDictionary *responseJSON = [NSDictionary dictionaryWithJSONData:data]; + if (!responseJSON) { + return nil; + } + ARDMessageResponse *response = [[ARDMessageResponse alloc] init]; + response.result = + [[self class] resultTypeFromString:responseJSON[kARDMessageResultKey]]; + return response; +} + +#pragma mark - Private + ++ (ARDMessageResultType)resultTypeFromString:(NSString *)resultString { + ARDMessageResultType result = kARDMessageResultTypeUnknown; + if ([resultString isEqualToString:@"SUCCESS"]) { + result = kARDMessageResultTypeSuccess; + } else if ([resultString isEqualToString:@"INVALID_CLIENT"]) { + result = kARDMessageResultTypeInvalidClient; + } else if ([resultString isEqualToString:@"INVALID_ROOM"]) { + result = kARDMessageResultTypeInvalidRoom; + } + return result; +} + +@end diff --git a/talk/examples/objc/AppRTCDemo/GAEChannelClient.h b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.h similarity index 64% rename from talk/examples/objc/AppRTCDemo/GAEChannelClient.h rename to talk/examples/objc/AppRTCDemo/ARDRegisterResponse.h index dbaecebd97..c26786c593 100644 --- a/talk/examples/objc/AppRTCDemo/GAEChannelClient.h +++ b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.h @@ -1,6 +1,6 @@ /* * libjingle - * Copyright 2013, Google Inc. + * Copyright 2014, Google Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -27,26 +27,23 @@ #import -// These methods will be called by the AppEngine chanel. The documentation -// for these methods is found here. (Yes, it is a JS API.) -// https://developers.google.com/appengine/docs/java/channel/javascript -@protocol GAEMessageHandler +typedef NS_ENUM(NSInteger, ARDRegisterResultType) { + kARDRegisterResultTypeUnknown, + kARDRegisterResultTypeSuccess, + kARDRegisterResultTypeFull +}; -- (void)onOpen; -- (void)onMessage:(NSDictionary*)data; -- (void)onClose; -- (void)onError:(int)code withDescription:(NSString*)description; - -@end - -// Initialize with a token for an AppRTC data channel. This will load -// ios_channel.html and use the token to establish a data channel between the -// application and AppEngine. -@interface GAEChannelClient : NSObject - -@property(nonatomic, weak) id delegate; - -- (instancetype)initWithToken:(NSString*)token - delegate:(id)delegate; +// Result of registering with the GAE server. +@interface ARDRegisterResponse : NSObject + +@property(nonatomic, readonly) ARDRegisterResultType result; +@property(nonatomic, readonly) BOOL isInitiator; +@property(nonatomic, readonly) NSString *roomId; +@property(nonatomic, readonly) NSString *clientId; +@property(nonatomic, readonly) NSArray *messages; +@property(nonatomic, readonly) NSURL *webSocketURL; +@property(nonatomic, readonly) NSURL *webSocketRestURL; + ++ (ARDRegisterResponse *)responseFromJSONData:(NSData *)data; @end diff --git a/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m new file mode 100644 index 0000000000..76eb15c671 --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m @@ -0,0 +1,111 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "ARDRegisterResponse.h" + +#import "ARDSignalingMessage.h" +#import "ARDUtilities.h" +#import "RTCICEServer+JSON.h" + +static NSString const *kARDRegisterResultKey = @"result"; +static NSString const *kARDRegisterResultParamsKey = @"params"; +static NSString const *kARDRegisterInitiatorKey = @"is_initiator"; +static NSString const *kARDRegisterRoomIdKey = @"room_id"; +static NSString const *kARDRegisterClientIdKey = @"client_id"; +static NSString const *kARDRegisterMessagesKey = @"messages"; +static NSString const *kARDRegisterWebSocketURLKey = @"wss_url"; +static NSString const *kARDRegisterWebSocketRestURLKey = @"wss_post_url"; + +@interface ARDRegisterResponse () + +@property(nonatomic, assign) ARDRegisterResultType result; +@property(nonatomic, assign) BOOL isInitiator; +@property(nonatomic, strong) NSString *roomId; +@property(nonatomic, strong) NSString *clientId; +@property(nonatomic, strong) NSArray *messages; +@property(nonatomic, strong) NSURL *webSocketURL; +@property(nonatomic, strong) NSURL *webSocketRestURL; + +@end + +@implementation ARDRegisterResponse + +@synthesize result = _result; +@synthesize isInitiator = _isInitiator; +@synthesize roomId = _roomId; +@synthesize clientId = _clientId; +@synthesize messages = _messages; +@synthesize webSocketURL = _webSocketURL; +@synthesize webSocketRestURL = _webSocketRestURL; + ++ (ARDRegisterResponse *)responseFromJSONData:(NSData *)data { + NSDictionary *responseJSON = [NSDictionary dictionaryWithJSONData:data]; + if (!responseJSON) { + return nil; + } + ARDRegisterResponse *response = [[ARDRegisterResponse alloc] init]; + NSString *resultString = responseJSON[kARDRegisterResultKey]; + response.result = [[self class] resultTypeFromString:resultString]; + NSDictionary *params = responseJSON[kARDRegisterResultParamsKey]; + + response.isInitiator = [params[kARDRegisterInitiatorKey] boolValue]; + response.roomId = params[kARDRegisterRoomIdKey]; + response.clientId = params[kARDRegisterClientIdKey]; + + // Parse messages. + NSArray *messages = params[kARDRegisterMessagesKey]; + NSMutableArray *signalingMessages = + [NSMutableArray arrayWithCapacity:messages.count]; + for (NSString *message in messages) { + ARDSignalingMessage *signalingMessage = + [ARDSignalingMessage messageFromJSONString:message]; + [signalingMessages addObject:signalingMessage]; + } + response.messages = signalingMessages; + + // Parse websocket urls. + NSString *webSocketURLString = params[kARDRegisterWebSocketURLKey]; + response.webSocketURL = [NSURL URLWithString:webSocketURLString]; + NSString *webSocketRestURLString = params[kARDRegisterWebSocketRestURLKey]; + response.webSocketRestURL = [NSURL URLWithString:webSocketRestURLString]; + + return response; +} + +#pragma mark - Private + ++ (ARDRegisterResultType)resultTypeFromString:(NSString *)resultString { + ARDRegisterResultType result = kARDRegisterResultTypeUnknown; + if ([resultString isEqualToString:@"SUCCESS"]) { + result = kARDRegisterResultTypeSuccess; + } else if ([resultString isEqualToString:@"FULL"]) { + result = kARDRegisterResultTypeFull; + } + return result; +} + +@end diff --git a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.h b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.h similarity index 55% rename from talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.h rename to talk/examples/objc/AppRTCDemo/ARDSignalingMessage.h index 98fe755ef6..b342694cfd 100644 --- a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.h +++ b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.h @@ -27,40 +27,40 @@ #import -// Used to log messages to destination like UI. -@protocol APPRTCLogger -- (void)logMessage:(NSString*)message; -@end +#import "RTCICECandidate.h" +#import "RTCSessionDescription.h" -@class RTCVideoTrack; -@class APPRTCConnectionManager; +typedef enum { + kARDSignalingMessageTypeCandidate, + kARDSignalingMessageTypeOffer, + kARDSignalingMessageTypeAnswer, + kARDSignalingMessageTypeBye, +} ARDSignalingMessageType; -// Used to provide AppRTC connection information. -@protocol APPRTCConnectionManagerDelegate +@interface ARDSignalingMessage : NSObject -- (void)connectionManager:(APPRTCConnectionManager*)manager - didReceiveLocalVideoTrack:(RTCVideoTrack*)localVideoTrack; +@property(nonatomic, readonly) ARDSignalingMessageType type; -- (void)connectionManager:(APPRTCConnectionManager*)manager - didReceiveRemoteVideoTrack:(RTCVideoTrack*)remoteVideoTrack; - -- (void)connectionManagerDidReceiveHangup:(APPRTCConnectionManager*)manager; - -- (void)connectionManager:(APPRTCConnectionManager*)manager - didErrorWithMessage:(NSString*)errorMessage; ++ (ARDSignalingMessage *)messageFromJSONString:(NSString *)jsonString; +- (NSData *)JSONData; @end -// Abstracts the network connection aspect of AppRTC. The delegate will receive -// information about connection status as changes occur. -@interface APPRTCConnectionManager : NSObject +@interface ARDICECandidateMessage : ARDSignalingMessage -@property(nonatomic, weak) id delegate; -@property(nonatomic, weak) id logger; +@property(nonatomic, readonly) RTCICECandidate *candidate; -- (instancetype)initWithDelegate:(id)delegate - logger:(id)logger; -- (BOOL)connectToRoomWithURL:(NSURL*)url; -- (void)disconnect; +- (instancetype)initWithCandidate:(RTCICECandidate *)candidate; @end + +@interface ARDSessionDescriptionMessage : ARDSignalingMessage + +@property(nonatomic, readonly) RTCSessionDescription *sessionDescription; + +- (instancetype)initWithDescription:(RTCSessionDescription *)description; + +@end + +@interface ARDByeMessage : ARDSignalingMessage +@end diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.m b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.m new file mode 100644 index 0000000000..38c4d5d151 --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.m @@ -0,0 +1,143 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "ARDSignalingMessage.h" + +#import "ARDUtilities.h" +#import "RTCICECandidate+JSON.h" +#import "RTCSessionDescription+JSON.h" + +static NSString const *kARDSignalingMessageTypeKey = @"type"; + +@implementation ARDSignalingMessage + +@synthesize type = _type; + +- (instancetype)initWithType:(ARDSignalingMessageType)type { + if (self = [super init]) { + _type = type; + } + return self; +} + +- (NSString *)description { + return [[NSString alloc] initWithData:[self JSONData] + encoding:NSUTF8StringEncoding]; +} + ++ (ARDSignalingMessage *)messageFromJSONString:(NSString *)jsonString { + NSDictionary *values = [NSDictionary dictionaryWithJSONString:jsonString]; + if (!values) { + NSLog(@"Error parsing signaling message JSON."); + return nil; + } + + NSString *typeString = values[kARDSignalingMessageTypeKey]; + ARDSignalingMessage *message = nil; + if ([typeString isEqualToString:@"candidate"]) { + RTCICECandidate *candidate = + [RTCICECandidate candidateFromJSONDictionary:values]; + message = [[ARDICECandidateMessage alloc] initWithCandidate:candidate]; + } else if ([typeString isEqualToString:@"offer"] || + [typeString isEqualToString:@"answer"]) { + RTCSessionDescription *description = + [RTCSessionDescription descriptionFromJSONDictionary:values]; + message = + [[ARDSessionDescriptionMessage alloc] initWithDescription:description]; + } else if ([typeString isEqualToString:@"bye"]) { + message = [[ARDByeMessage alloc] init]; + } else { + NSLog(@"Unexpected type: %@", typeString); + } + return message; +} + +- (NSData *)JSONData { + return nil; +} + +@end + +@implementation ARDICECandidateMessage + +@synthesize candidate = _candidate; + +- (instancetype)initWithCandidate:(RTCICECandidate *)candidate { + if (self = [super initWithType:kARDSignalingMessageTypeCandidate]) { + _candidate = candidate; + } + return self; +} + +- (NSData *)JSONData { + return [_candidate JSONData]; +} + +@end + +@implementation ARDSessionDescriptionMessage + +@synthesize sessionDescription = _sessionDescription; + +- (instancetype)initWithDescription:(RTCSessionDescription *)description { + ARDSignalingMessageType type = kARDSignalingMessageTypeOffer; + NSString *typeString = description.type; + if ([typeString isEqualToString:@"offer"]) { + type = kARDSignalingMessageTypeOffer; + } else if ([typeString isEqualToString:@"answer"]) { + type = kARDSignalingMessageTypeAnswer; + } else { + NSAssert(NO, @"Unexpected type: %@", typeString); + } + if (self = [super initWithType:type]) { + _sessionDescription = description; + } + return self; +} + +- (NSData *)JSONData { + return [_sessionDescription JSONData]; +} + +@end + +@implementation ARDByeMessage + +- (instancetype)init { + return [super initWithType:kARDSignalingMessageTypeBye]; +} + +- (NSData *)JSONData { + NSDictionary *message = @{ + @"type": @"bye" + }; + return [NSJSONSerialization dataWithJSONObject:message + options:NSJSONWritingPrettyPrinted + error:NULL]; +} + +@end diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.m b/talk/examples/objc/AppRTCDemo/ARDSignalingParams.m deleted file mode 100644 index 58c8684b8a..0000000000 --- a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.m +++ /dev/null @@ -1,130 +0,0 @@ -/* - * libjingle - * Copyright 2014, Google Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#import "ARDSignalingParams.h" - -#import "ARDUtilities.h" -#import "RTCICEServer+JSON.h" -#import "RTCMediaConstraints+JSON.h" - -static NSString const *kARDSignalingParamsErrorKey = @"error"; -static NSString const *kARDSignalingParamsErrorMessagesKey = @"error_messages"; -static NSString const *kARDSignalingParamsInitiatorKey = @"initiator"; -static NSString const *kARDSignalingParamsPeerConnectionConfigKey = - @"pc_config"; -static NSString const *kARDSignalingParamsICEServersKey = @"iceServers"; -static NSString const *kARDSignalingParamsMediaConstraintsKey = - @"media_constraints"; -static NSString const *kARDSignalingParamsMediaConstraintsVideoKey = - @"video"; -static NSString const *kARDSignalingParamsTokenKey = @"token"; -static NSString const *kARDSignalingParamsTurnRequestUrlKey = @"turn_url"; - -@interface ARDSignalingParams () - -@property(nonatomic, strong) NSArray *errorMessages; -@property(nonatomic, strong) RTCMediaConstraints *offerConstraints; -@property(nonatomic, strong) RTCMediaConstraints *mediaConstraints; -@property(nonatomic, strong) NSMutableArray *iceServers; -@property(nonatomic, strong) NSURL *signalingServerURL; -@property(nonatomic, strong) NSURL *turnRequestURL; -@property(nonatomic, strong) NSString *channelToken; - -@end - -@implementation ARDSignalingParams - -@synthesize errorMessages = _errorMessages; -@synthesize isInitiator = _isInitiator; -@synthesize offerConstraints = _offerConstraints; -@synthesize mediaConstraints = _mediaConstraints; -@synthesize iceServers = _iceServers; -@synthesize signalingServerURL = _signalingServerURL; - -+ (ARDSignalingParams *)paramsFromJSONData:(NSData *)data { - NSDictionary *paramsJSON = [NSDictionary dictionaryWithJSONData:data]; - if (!paramsJSON) { - return nil; - } - ARDSignalingParams *params = [[ARDSignalingParams alloc] init]; - - // Parse errors. - BOOL hasError = NO; - NSArray *errorMessages = paramsJSON[kARDSignalingParamsErrorMessagesKey]; - if (errorMessages.count > 0) { - params.errorMessages = errorMessages; - return params; - } - - // Parse ICE servers. - NSString *peerConnectionConfigString = - paramsJSON[kARDSignalingParamsPeerConnectionConfigKey]; - NSDictionary *peerConnectionConfig = - [NSDictionary dictionaryWithJSONString:peerConnectionConfigString]; - NSArray *iceServerJSONArray = - peerConnectionConfig[kARDSignalingParamsICEServersKey]; - NSMutableArray *iceServers = [NSMutableArray array]; - for (NSDictionary *iceServerJSON in iceServerJSONArray) { - RTCICEServer *iceServer = - [RTCICEServer serverFromJSONDictionary:iceServerJSON]; - [iceServers addObject:iceServer]; - } - params.iceServers = iceServers; - - // Parse initiator. - BOOL isInitiator = [paramsJSON[kARDSignalingParamsInitiatorKey] boolValue]; - params.isInitiator = isInitiator; - - // Parse video constraints. - RTCMediaConstraints *videoConstraints = nil; - NSString *mediaConstraintsJSONString = - paramsJSON[kARDSignalingParamsMediaConstraintsKey]; - NSDictionary *mediaConstraintsJSON = - [NSDictionary dictionaryWithJSONString:mediaConstraintsJSONString]; - id videoJSON = - mediaConstraintsJSON[kARDSignalingParamsMediaConstraintsVideoKey]; - if ([videoJSON isKindOfClass:[NSDictionary class]]) { - videoConstraints = - [RTCMediaConstraints constraintsFromJSONDictionary:videoJSON]; - } else if ([videoJSON isKindOfClass:[NSNumber class]] && - [videoJSON boolValue]) { - videoConstraints = [[RTCMediaConstraints alloc] init]; - } - params.mediaConstraints = videoConstraints; - - // Parse channel token. - NSString *token = paramsJSON[kARDSignalingParamsTokenKey]; - params.channelToken = token; - - // Parse turn request url. - params.turnRequestURL = - [NSURL URLWithString:paramsJSON[kARDSignalingParamsTurnRequestUrlKey]]; - - return params; -} - -@end diff --git a/talk/examples/objc/AppRTCDemo/ARDUtilities.h b/talk/examples/objc/AppRTCDemo/ARDUtilities.h index ca4d1d7304..f2219f30c4 100644 --- a/talk/examples/objc/AppRTCDemo/ARDUtilities.h +++ b/talk/examples/objc/AppRTCDemo/ARDUtilities.h @@ -38,9 +38,15 @@ @interface NSURLConnection (ARDUtilities) // Issues an asynchronous request that calls back on main queue. -+ (void)sendAsynchronousRequest:(NSURLRequest *)request - completionHandler:(void (^)(NSURLResponse *response, - NSData *data, - NSError *error))completionHandler; ++ (void)sendAsyncRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLResponse *response, + NSData *data, + NSError *error))completionHandler; + +// Posts data to the specified URL. ++ (void)sendAsyncPostToURL:(NSURL *)url + withData:(NSData *)data + completionHandler:(void (^)(BOOL succeeded, + NSData *data))completionHandler; @end diff --git a/talk/examples/objc/AppRTCDemo/ARDUtilities.m b/talk/examples/objc/AppRTCDemo/ARDUtilities.m index 937013ea65..54b1c51599 100644 --- a/talk/examples/objc/AppRTCDemo/ARDUtilities.m +++ b/talk/examples/objc/AppRTCDemo/ARDUtilities.m @@ -55,17 +55,55 @@ @implementation NSURLConnection (ARDUtilities) -+ (void)sendAsynchronousRequest:(NSURLRequest *)request - completionHandler:(void (^)(NSURLResponse *response, - NSData *data, - NSError *error))completionHandler { ++ (void)sendAsyncRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLResponse *response, + NSData *data, + NSError *error))completionHandler { // Kick off an async request which will call back on main thread. [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { - completionHandler(response, data, error); + if (completionHandler) { + completionHandler(response, data, error); + } + }]; +} + +// Posts data to the specified URL. ++ (void)sendAsyncPostToURL:(NSURL *)url + withData:(NSData *)data + completionHandler:(void (^)(BOOL succeeded, + NSData *data))completionHandler { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBody = data; + [[self class] sendAsyncRequest:request + completionHandler:^(NSURLResponse *response, + NSData *data, + NSError *error) { + if (error) { + NSLog(@"Error posting data: %@", error.localizedDescription); + if (completionHandler) { + completionHandler(NO, data); + } + return; + } + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode != 200) { + NSString *serverResponse = data.length > 0 ? + [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : + nil; + NSLog(@"Received bad response: %@", serverResponse); + if (completionHandler) { + completionHandler(NO, data); + } + return; + } + if (completionHandler) { + completionHandler(YES, data); + } }]; } diff --git a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h new file mode 100644 index 0000000000..06c65201b8 --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h @@ -0,0 +1,75 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +#import "ARDSignalingMessage.h" + +typedef NS_ENUM(NSInteger, ARDWebSocketChannelState) { + // State when disconnected. + kARDWebSocketChannelStateClosed, + // State when connection is established but not ready for use. + kARDWebSocketChannelStateOpen, + // State when connection is established and registered. + kARDWebSocketChannelStateRegistered, + // State when connection encounters a fatal error. + kARDWebSocketChannelStateError +}; + +@class ARDWebSocketChannel; +@protocol ARDWebSocketChannelDelegate + +- (void)channel:(ARDWebSocketChannel *)channel + didChangeState:(ARDWebSocketChannelState)state; + +- (void)channel:(ARDWebSocketChannel *)channel + didReceiveMessage:(ARDSignalingMessage *)message; + +@end + +// Wraps a WebSocket connection to the AppRTC WebSocket server. +@interface ARDWebSocketChannel : NSObject + +@property(nonatomic, readonly) NSString *roomId; +@property(nonatomic, readonly) NSString *clientId; +@property(nonatomic, readonly) ARDWebSocketChannelState state; +@property(nonatomic, weak) id delegate; + +- (instancetype)initWithURL:(NSURL *)url + restURL:(NSURL *)restURL + delegate:(id)delegate; + +// Registers with the WebSocket server for the given room and client id once +// the web socket connection is open. +- (void)registerForRoomId:(NSString *)roomId + clientId:(NSString *)clientId; + +// Sends data over the WebSocket connection if registered, otherwise POSTs to +// the web socket server instead. +- (void)sendData:(NSData *)data; + +@end diff --git a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m new file mode 100644 index 0000000000..1707201724 --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m @@ -0,0 +1,213 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "ARDWebSocketChannel.h" + +#import "ARDUtilities.h" +#import "SRWebSocket.h" + +// TODO(tkchin): move these to a configuration object. +static NSString const *kARDWSSMessageErrorKey = @"error"; +static NSString const *kARDWSSMessagePayloadKey = @"msg"; + +@interface ARDWebSocketChannel () +@end + +@implementation ARDWebSocketChannel { + NSURL *_url; + NSURL *_restURL; + SRWebSocket *_socket; +} + +@synthesize delegate = _delegate; +@synthesize state = _state; +@synthesize roomId = _roomId; +@synthesize clientId = _clientId; + +- (instancetype)initWithURL:(NSURL *)url + restURL:(NSURL *)restURL + delegate:(id)delegate { + if (self = [super init]) { + _url = url; + _restURL = restURL; + _delegate = delegate; + _socket = [[SRWebSocket alloc] initWithURL:url]; + _socket.delegate = self; + NSLog(@"Opening WebSocket."); + [_socket open]; + } + return self; +} + +- (void)dealloc { + [self disconnect]; +} + +- (void)setState:(ARDWebSocketChannelState)state { + if (_state == state) { + return; + } + _state = state; + [_delegate channel:self didChangeState:_state]; +} + +- (void)registerForRoomId:(NSString *)roomId + clientId:(NSString *)clientId { + NSParameterAssert(roomId.length); + NSParameterAssert(clientId.length); + _roomId = roomId; + _clientId = clientId; + if (_state == kARDWebSocketChannelStateOpen) { + [self registerWithCollider]; + } +} + +- (void)sendData:(NSData *)data { + NSParameterAssert(_clientId.length); + NSParameterAssert(_roomId.length); + if (_state == kARDWebSocketChannelStateRegistered) { + NSString *payload = + [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSDictionary *message = @{ + @"cmd": @"send", + @"msg": payload, + }; + NSData *messageJSONObject = + [NSJSONSerialization dataWithJSONObject:message + options:NSJSONWritingPrettyPrinted + error:nil]; + NSString *messageString = + [[NSString alloc] initWithData:messageJSONObject + encoding:NSUTF8StringEncoding]; + NSLog(@"C->WSS: %@", messageString); + [_socket send:messageString]; + } else { + NSString *dataString = + [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSLog(@"C->WSS POST: %@", dataString); + NSString *urlString = + [NSString stringWithFormat:@"%@/%@/%@", + [_restURL absoluteString], _roomId, _clientId]; + NSURL *url = [NSURL URLWithString:urlString]; + [NSURLConnection sendAsyncPostToURL:url + withData:data + completionHandler:nil]; + } +} + +- (void)disconnect { + if (_state == kARDWebSocketChannelStateClosed || + _state == kARDWebSocketChannelStateError) { + return; + } + [_socket close]; + NSLog(@"C->WSS DELETE rid:%@ cid:%@", _roomId, _clientId); + NSString *urlString = + [NSString stringWithFormat:@"%@/%@/%@", + [_restURL absoluteString], _roomId, _clientId]; + NSURL *url = [NSURL URLWithString:urlString]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.HTTPMethod = @"DELETE"; + request.HTTPBody = nil; + [NSURLConnection sendAsyncRequest:request completionHandler:nil]; +} + +#pragma mark - SRWebSocketDelegate + +- (void)webSocketDidOpen:(SRWebSocket *)webSocket { + NSLog(@"WebSocket connection opened."); + self.state = kARDWebSocketChannelStateOpen; + if (_roomId.length && _clientId.length) { + [self registerWithCollider]; + } +} + +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message { + NSString *messageString = message; + NSData *messageData = [messageString dataUsingEncoding:NSUTF8StringEncoding]; + id jsonObject = [NSJSONSerialization JSONObjectWithData:messageData + options:0 + error:nil]; + if (![jsonObject isKindOfClass:[NSDictionary class]]) { + NSLog(@"Unexpected message: %@", jsonObject); + return; + } + NSDictionary *wssMessage = jsonObject; + NSString *errorString = wssMessage[kARDWSSMessageErrorKey]; + if (errorString.length) { + NSLog(@"WSS error: %@", errorString); + return; + } + NSString *payload = wssMessage[kARDWSSMessagePayloadKey]; + ARDSignalingMessage *signalingMessage = + [ARDSignalingMessage messageFromJSONString:payload]; + NSLog(@"WSS->C: %@", payload); + [_delegate channel:self didReceiveMessage:signalingMessage]; +} + +- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error { + NSLog(@"WebSocket error: %@", error); + self.state = kARDWebSocketChannelStateError; +} + +- (void)webSocket:(SRWebSocket *)webSocket + didCloseWithCode:(NSInteger)code + reason:(NSString *)reason + wasClean:(BOOL)wasClean { + NSLog(@"WebSocket closed with code: %ld reason:%@ wasClean:%d", + (long)code, reason, wasClean); + NSParameterAssert(_state != kARDWebSocketChannelStateError); + self.state = kARDWebSocketChannelStateClosed; +} + +#pragma mark - Private + +- (void)registerWithCollider { + if (_state == kARDWebSocketChannelStateRegistered) { + return; + } + NSParameterAssert(_roomId.length); + NSParameterAssert(_clientId.length); + NSDictionary *registerMessage = @{ + @"cmd": @"register", + @"roomid" : _roomId, + @"clientid" : _clientId, + }; + NSData *message = + [NSJSONSerialization dataWithJSONObject:registerMessage + options:NSJSONWritingPrettyPrinted + error:nil]; + NSString *messageString = + [[NSString alloc] initWithData:message encoding:NSUTF8StringEncoding]; + NSLog(@"Registering on WSS for rid:%@ cid:%@", _roomId, _clientId); + // Registration can fail if server rejects it. For example, if the room is + // full. + [_socket send:messageString]; + self.state = kARDWebSocketChannelStateRegistered; +} + +@end diff --git a/talk/examples/objc/AppRTCDemo/GAEChannelClient.m b/talk/examples/objc/AppRTCDemo/GAEChannelClient.m deleted file mode 100644 index a95e99a8d6..0000000000 --- a/talk/examples/objc/AppRTCDemo/GAEChannelClient.m +++ /dev/null @@ -1,167 +0,0 @@ -/* - * libjingle - * Copyright 2013, Google Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#import "GAEChannelClient.h" - -#import "RTCPeerConnectionFactory.h" - -#if TARGET_OS_IPHONE - -#import - -@interface GAEChannelClient () - -@property(nonatomic, strong) UIWebView* webView; - -#else - -#import - -@interface GAEChannelClient () - -@property(nonatomic, strong) WebView* webView; - -#endif - -@end - -@implementation GAEChannelClient - -- (instancetype)initWithToken:(NSString*)token - delegate:(id)delegate { - NSParameterAssert([token length] > 0); - NSParameterAssert(delegate); - self = [super init]; - if (self) { -#if TARGET_OS_IPHONE - _webView = [[UIWebView alloc] init]; - _webView.delegate = self; -#else - _webView = [[WebView alloc] init]; - _webView.policyDelegate = self; -#endif - _delegate = delegate; - NSString* htmlPath = - [[NSBundle mainBundle] pathForResource:@"channel" ofType:@"html"]; - NSURL* htmlUrl = [NSURL fileURLWithPath:htmlPath]; - NSString* path = [NSString - stringWithFormat:@"%@?token=%@", [htmlUrl absoluteString], token]; -#if TARGET_OS_IPHONE - [_webView -#else - [[_webView mainFrame] -#endif - loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:path]]]; - } - return self; -} - -- (void)dealloc { -#if TARGET_OS_IPHONE - _webView.delegate = nil; - [_webView stopLoading]; -#else - _webView.policyDelegate = nil; - [[_webView mainFrame] stopLoading]; -#endif -} - -#if TARGET_OS_IPHONE -#pragma mark - UIWebViewDelegate - -- (BOOL)webView:(UIWebView*)webView - shouldStartLoadWithRequest:(NSURLRequest*)request - navigationType:(UIWebViewNavigationType)navigationType { -#else -// WebPolicyDelegate is an informal delegate. -#pragma mark - WebPolicyDelegate - -- (void)webView:(WebView*)webView - decidePolicyForNavigationAction:(NSDictionary*)actionInformation - request:(NSURLRequest*)request - frame:(WebFrame*)frame - decisionListener:(id)listener { -#endif - NSString* scheme = [request.URL scheme]; - NSAssert(scheme, @"scheme is nil: %@", request); - if (![scheme isEqualToString:@"js-frame"]) { -#if TARGET_OS_IPHONE - return YES; -#else - [listener use]; - return; -#endif - } - dispatch_async(dispatch_get_main_queue(), ^{ - NSString* queuedMessage = [webView - stringByEvaluatingJavaScriptFromString:@"popQueuedMessage();"]; - NSAssert([queuedMessage length], @"Empty queued message from JS"); - - NSDictionary* queuedMessageDict = - [GAEChannelClient jsonStringToDictionary:queuedMessage]; - NSString* method = queuedMessageDict[@"type"]; - NSAssert(method, @"Missing method: %@", queuedMessageDict); - NSDictionary* payload = queuedMessageDict[@"payload"]; // May be nil. - - if ([method isEqualToString:@"onopen"]) { - [self.delegate onOpen]; - } else if ([method isEqualToString:@"onmessage"]) { - NSDictionary* payloadData = - [GAEChannelClient jsonStringToDictionary:payload[@"data"]]; - [self.delegate onMessage:payloadData]; - } else if ([method isEqualToString:@"onclose"]) { - [self.delegate onClose]; - } else if ([method isEqualToString:@"onerror"]) { - NSNumber* codeNumber = payload[@"code"]; - int code = [codeNumber intValue]; - NSAssert([codeNumber isEqualToNumber:[NSNumber numberWithInt:code]], - @"Unexpected non-integral code: %@", payload); - [self.delegate onError:code withDescription:payload[@"description"]]; - } else { - NSAssert(NO, @"Invalid message sent from UIWebView: %@", queuedMessage); - } - }); -#if TARGET_OS_IPHONE - return NO; -#else - [listener ignore]; - return; -#endif -} - -#pragma mark - Private - -+ (NSDictionary*)jsonStringToDictionary:(NSString*)str { - NSData* data = [str dataUsingEncoding:NSUTF8StringEncoding]; - NSError* error; - NSDictionary* dict = - [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - NSAssert(!error, @"Invalid JSON? %@", str); - return dict; -} - -@end diff --git a/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m b/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m index 62817a5fed..85cf95bd24 100644 --- a/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m +++ b/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m @@ -50,7 +50,16 @@ static NSString const *kRTCICECandidateSdpKey = @"candidate"; kRTCICECandidateMidKey : self.sdpMid, kRTCICECandidateSdpKey : self.sdp }; - return [NSJSONSerialization dataWithJSONObject:json options:0 error:nil]; + NSError *error = nil; + NSData *data = + [NSJSONSerialization dataWithJSONObject:json + options:NSJSONWritingPrettyPrinted + error:&error]; + if (error) { + NSLog(@"Error serializing JSON: %@", error); + return nil; + } + return data; } @end diff --git a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h index d8a72076ca..2fb2fa0aa1 100644 --- a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h +++ b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h @@ -31,6 +31,6 @@ + (RTCICEServer *)serverFromJSONDictionary:(NSDictionary *)dictionary; // CEOD provides different JSON, and this parses that. -+ (RTCICEServer *)serverFromCEODJSONDictionary:(NSDictionary *)dictionary; ++ (NSArray *)serversFromCEODJSONDictionary:(NSDictionary *)dictionary; @end diff --git a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m index 29321f65ff..9465213856 100644 --- a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m +++ b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m @@ -46,14 +46,19 @@ static NSString const *kRTCICEServerCredentialKey = @"credential"; password:credential]; } -+ (RTCICEServer *)serverFromCEODJSONDictionary:(NSDictionary *)dictionary { ++ (NSArray *)serversFromCEODJSONDictionary:(NSDictionary *)dictionary { NSString *username = dictionary[kRTCICEServerUsernameKey]; NSString *password = dictionary[kRTCICEServerPasswordKey]; NSArray *uris = dictionary[kRTCICEServerUrisKey]; - NSParameterAssert(uris.count > 0); - return [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:uris[0]] - username:username - password:password]; + NSMutableArray *servers = [NSMutableArray arrayWithCapacity:uris.count]; + for (NSString *uri in uris) { + RTCICEServer *server = + [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:uri] + username:username + password:password]; + [servers addObject:server]; + } + return servers; } @end diff --git a/talk/examples/objc/AppRTCDemo/channel.html b/talk/examples/objc/AppRTCDemo/channel.html deleted file mode 100644 index 86846dd687..0000000000 --- a/talk/examples/objc/AppRTCDemo/channel.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - diff --git a/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m b/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m index d8d9714962..3d60ee77f7 100644 --- a/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m +++ b/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m @@ -32,38 +32,28 @@ #import "APPRTCViewController.h" #import -#import "APPRTCConnectionManager.h" +#import "ARDAppClient.h" #import "RTCEAGLVideoView.h" #import "RTCVideoTrack.h" // Padding space for local video view with its parent. static CGFloat const kLocalViewPadding = 20; -@interface APPRTCViewController () - +@interface APPRTCViewController () @property(nonatomic, assign) UIInterfaceOrientation statusBarOrientation; @property(nonatomic, strong) RTCEAGLVideoView* localVideoView; @property(nonatomic, strong) RTCEAGLVideoView* remoteVideoView; @end @implementation APPRTCViewController { - APPRTCConnectionManager* _connectionManager; + ARDAppClient *_client; RTCVideoTrack* _localVideoTrack; RTCVideoTrack* _remoteVideoTrack; CGSize _localVideoSize; CGSize _remoteVideoSize; } -- (instancetype)initWithNibName:(NSString*)nibName - bundle:(NSBundle*)bundle { - if (self = [super initWithNibName:nibName bundle:bundle]) { - _connectionManager = - [[APPRTCConnectionManager alloc] initWithDelegate:self - logger:self]; - } - return self; -} - - (void)viewDidLoad { [super viewDidLoad]; @@ -96,48 +86,46 @@ static CGFloat const kLocalViewPadding = 20; } - (void)applicationWillResignActive:(UIApplication*)application { - [self logMessage:@"Application lost focus, connection broken."]; [self disconnect]; } -#pragma mark - APPRTCConnectionManagerDelegate +#pragma mark - ARDAppClientDelegate -- (void)connectionManager:(APPRTCConnectionManager*)manager - didReceiveLocalVideoTrack:(RTCVideoTrack*)localVideoTrack { +- (void)appClient:(ARDAppClient *)client + didChangeState:(ARDAppClientState)state { + switch (state) { + case kARDAppClientStateConnected: + NSLog(@"Client connected."); + break; + case kARDAppClientStateConnecting: + NSLog(@"Client connecting."); + break; + case kARDAppClientStateDisconnected: + NSLog(@"Client disconnected."); + [self resetUI]; + break; + } +} + +- (void)appClient:(ARDAppClient *)client + didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack { _localVideoTrack = localVideoTrack; [_localVideoTrack addRenderer:self.localVideoView]; self.localVideoView.hidden = NO; } -- (void)connectionManager:(APPRTCConnectionManager*)manager - didReceiveRemoteVideoTrack:(RTCVideoTrack*)remoteVideoTrack { +- (void)appClient:(ARDAppClient *)client + didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack { _remoteVideoTrack = remoteVideoTrack; [_remoteVideoTrack addRenderer:self.remoteVideoView]; } -- (void)connectionManagerDidReceiveHangup:(APPRTCConnectionManager*)manager { - [self showAlertWithMessage:@"Remote hung up."]; +- (void)appClient:(ARDAppClient *)client + didError:(NSError *)error { + [self showAlertWithMessage:[NSString stringWithFormat:@"%@", error]]; [self disconnect]; } -- (void)connectionManager:(APPRTCConnectionManager*)manager - didErrorWithMessage:(NSString*)message { - [self showAlertWithMessage:message]; - [self disconnect]; -} - -#pragma mark - APPRTCLogger - -- (void)logMessage:(NSString*)message { - dispatch_async(dispatch_get_main_queue(), ^{ - NSString* output = - [NSString stringWithFormat:@"%@\n%@", self.logView.text, message]; - self.logView.text = output; - [self.logView - scrollRangeToVisible:NSMakeRange([self.logView.text length], 0)]; - }); -} - #pragma mark - RTCEAGLVideoViewDelegate - (void)videoView:(RTCEAGLVideoView*)videoView @@ -162,9 +150,10 @@ static CGFloat const kLocalViewPadding = 20; textField.hidden = YES; self.instructionsView.hidden = YES; self.logView.hidden = NO; - NSString* url = - [NSString stringWithFormat:@"https://apprtc.appspot.com/?r=%@", room]; - [_connectionManager connectToRoomWithURL:[NSURL URLWithString:url]]; + [_client disconnect]; + // TODO(tkchin): support reusing the same client object. + _client = [[ARDAppClient alloc] initWithDelegate:self]; + [_client connectToRoomWithId:room options:nil]; [self setupCaptureSession]; } @@ -179,7 +168,7 @@ static CGFloat const kLocalViewPadding = 20; - (void)disconnect { [self resetUI]; - [_connectionManager disconnect]; + [_client disconnect]; } - (void)showAlertWithMessage:(NSString*)message { diff --git a/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m b/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m index 08acac93e9..40d130724e 100644 --- a/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m +++ b/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m @@ -28,7 +28,7 @@ #import "APPRTCViewController.h" #import -#import "APPRTCConnectionManager.h" +#import "ARDAppClient.h" #import "RTCNSGLVideoView.h" #import "RTCVideoTrack.h" @@ -222,26 +222,16 @@ static NSUInteger const kLogViewHeight = 280; @end @interface APPRTCViewController () - + @property(nonatomic, readonly) APPRTCMainView* mainView; @end @implementation APPRTCViewController { - APPRTCConnectionManager* _connectionManager; + ARDAppClient* _client; RTCVideoTrack* _localVideoTrack; RTCVideoTrack* _remoteVideoTrack; } -- (instancetype)initWithNibName:(NSString*)nibName - bundle:(NSBundle*)bundle { - if (self = [super initWithNibName:nibName bundle:bundle]) { - _connectionManager = - [[APPRTCConnectionManager alloc] initWithDelegate:self - logger:self]; - } - return self; -} - - (void)dealloc { [self disconnect]; } @@ -257,43 +247,50 @@ static NSUInteger const kLogViewHeight = 280; [self disconnect]; } -#pragma mark - APPRTCConnectionManagerDelegate +#pragma mark - ARDAppClientDelegate -- (void)connectionManager:(APPRTCConnectionManager*)manager - didReceiveLocalVideoTrack:(RTCVideoTrack*)localVideoTrack { +- (void)appClient:(ARDAppClient *)client + didChangeState:(ARDAppClientState)state { + switch (state) { + case kARDAppClientStateConnected: + NSLog(@"Client connected."); + break; + case kARDAppClientStateConnecting: + NSLog(@"Client connecting."); + break; + case kARDAppClientStateDisconnected: + NSLog(@"Client disconnected."); + [self resetUI]; + _client = nil; + break; + } +} + +- (void)appClient:(ARDAppClient *)client + didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack { _localVideoTrack = localVideoTrack; } -- (void)connectionManager:(APPRTCConnectionManager*)manager - didReceiveRemoteVideoTrack:(RTCVideoTrack*)remoteVideoTrack { +- (void)appClient:(ARDAppClient *)client + didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack { _remoteVideoTrack = remoteVideoTrack; [_remoteVideoTrack addRenderer:self.mainView.remoteVideoView]; } -- (void)connectionManagerDidReceiveHangup:(APPRTCConnectionManager*)manager { - [self showAlertWithMessage:@"Remote closed connection"]; +- (void)appClient:(ARDAppClient *)client + didError:(NSError *)error { + [self showAlertWithMessage:[NSString stringWithFormat:@"%@", error]]; [self disconnect]; } -- (void)connectionManager:(APPRTCConnectionManager*)manager - didErrorWithMessage:(NSString*)message { - [self showAlertWithMessage:message]; - [self disconnect]; -} - -#pragma mark - APPRTCLogger - -- (void)logMessage:(NSString*)message { - [self.mainView displayLogMessage:message]; -} - #pragma mark - APPRTCMainViewDelegate - (void)appRTCMainView:(APPRTCMainView*)mainView didEnterRoomId:(NSString*)roomId { - NSString* urlString = - [NSString stringWithFormat:@"https://apprtc.appspot.com/?r=%@", roomId]; - [_connectionManager connectToRoomWithURL:[NSURL URLWithString:urlString]]; + [_client disconnect]; + ARDAppClient *client = [[ARDAppClient alloc] initWithDelegate:self]; + [client connectToRoomWithId:roomId options:nil]; + _client = client; } #pragma mark - Private @@ -308,11 +305,15 @@ static NSUInteger const kLogViewHeight = 280; [alert runModal]; } -- (void)disconnect { +- (void)resetUI { [_remoteVideoTrack removeRenderer:self.mainView.remoteVideoView]; _remoteVideoTrack = nil; [self.mainView.remoteVideoView renderFrame:nil]; - [_connectionManager disconnect]; +} + +- (void)disconnect { + [self resetUI]; + [_client disconnect]; } @end diff --git a/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/LICENSE b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/LICENSE new file mode 100644 index 0000000000..c01a79c3bd --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/LICENSE @@ -0,0 +1,15 @@ + + Copyright 2012 Square Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.h b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.h new file mode 100644 index 0000000000..5cce725a34 --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.h @@ -0,0 +1,132 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import + +typedef enum { + SR_CONNECTING = 0, + SR_OPEN = 1, + SR_CLOSING = 2, + SR_CLOSED = 3, +} SRReadyState; + +typedef enum SRStatusCode : NSInteger { + SRStatusCodeNormal = 1000, + SRStatusCodeGoingAway = 1001, + SRStatusCodeProtocolError = 1002, + SRStatusCodeUnhandledType = 1003, + // 1004 reserved. + SRStatusNoStatusReceived = 1005, + // 1004-1006 reserved. + SRStatusCodeInvalidUTF8 = 1007, + SRStatusCodePolicyViolated = 1008, + SRStatusCodeMessageTooBig = 1009, +} SRStatusCode; + +@class SRWebSocket; + +extern NSString *const SRWebSocketErrorDomain; +extern NSString *const SRHTTPResponseErrorKey; + +#pragma mark - SRWebSocketDelegate + +@protocol SRWebSocketDelegate; + +#pragma mark - SRWebSocket + +@interface SRWebSocket : NSObject + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, readonly) SRReadyState readyState; +@property (nonatomic, readonly, retain) NSURL *url; + +// This returns the negotiated protocol. +// It will be nil until after the handshake completes. +@property (nonatomic, readonly, copy) NSString *protocol; + +// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. +- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +- (id)initWithURLRequest:(NSURLRequest *)request; + +// Some helper constructors. +- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +- (id)initWithURL:(NSURL *)url; + +// Delegate queue will be dispatch_main_queue by default. +// You cannot set both OperationQueue and dispatch_queue. +- (void)setDelegateOperationQueue:(NSOperationQueue*) queue; +- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue; + +// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes. +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; + +// SRWebSockets are intended for one-time-use only. Open should be called once and only once. +- (void)open; + +- (void)close; +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; + +// Send a UTF8 String or Data. +- (void)send:(id)data; + +// Send Data (can be nil) in a ping message. +- (void)sendPing:(NSData *)data; + +@end + +#pragma mark - SRWebSocketDelegate + +@protocol SRWebSocketDelegate + +// message will either be an NSString if the server is using text +// or NSData if the server is using binary. +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message; + +@optional + +- (void)webSocketDidOpen:(SRWebSocket *)webSocket; +- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error; +- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean; +- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload; + +@end + +#pragma mark - NSURLRequest (CertificateAdditions) + +@interface NSURLRequest (CertificateAdditions) + +@property (nonatomic, retain, readonly) NSArray *SR_SSLPinnedCertificates; + +@end + +#pragma mark - NSMutableURLRequest (CertificateAdditions) + +@interface NSMutableURLRequest (CertificateAdditions) + +@property (nonatomic, retain) NSArray *SR_SSLPinnedCertificates; + +@end + +#pragma mark - NSRunLoop (SRWebSocket) + +@interface NSRunLoop (SRWebSocket) + ++ (NSRunLoop *)SR_networkRunLoop; + +@end diff --git a/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.m b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.m new file mode 100644 index 0000000000..b8add7f84a --- /dev/null +++ b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.m @@ -0,0 +1,1761 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +#import "SRWebSocket.h" + +#if TARGET_OS_IPHONE +#define HAS_ICU +#endif + +#ifdef HAS_ICU +#import +#endif + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +#import +#import + +#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE +#define sr_dispatch_retain(x) +#define sr_dispatch_release(x) +#define maybe_bridge(x) ((__bridge void *) x) +#else +#define sr_dispatch_retain(x) dispatch_retain(x) +#define sr_dispatch_release(x) dispatch_release(x) +#define maybe_bridge(x) (x) +#endif + +#if !__has_feature(objc_arc) +#error SocketRocket must be compiled with ARC enabled +#endif + + +typedef enum { + SROpCodeTextFrame = 0x1, + SROpCodeBinaryFrame = 0x2, + // 3-7 reserved. + SROpCodeConnectionClose = 0x8, + SROpCodePing = 0x9, + SROpCodePong = 0xA, + // B-F reserved. +} SROpCode; + +typedef struct { + BOOL fin; +// BOOL rsv1; +// BOOL rsv2; +// BOOL rsv3; + uint8_t opcode; + BOOL masked; + uint64_t payload_length; +} frame_header; + +static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +static inline int32_t validate_dispatch_data_partial_string(NSData *data); +static inline void SRFastLog(NSString *format, ...); + +@interface NSData (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSString (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSURL (SRWebSocket) + +// The origin isn't really applicable for a native application. +// So instead, just map ws -> http and wss -> https. +- (NSString *)SR_origin; + +@end + + +@interface _SRRunLoopThread : NSThread + +@property (nonatomic, readonly) NSRunLoop *runLoop; + +@end + + +static NSString *newSHA1String(const char *bytes, size_t length) { + uint8_t md[CC_SHA1_DIGEST_LENGTH]; + + assert(length >= 0); + assert(length <= UINT32_MAX); + CC_SHA1(bytes, (CC_LONG)length, md); + + NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; + + if ([data respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { + return [data base64EncodedStringWithOptions:0]; + } + + return [data base64Encoding]; +} + +@implementation NSData (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.bytes, self.length); +} + +@end + + +@implementation NSString (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.UTF8String, self.length); +} + +@end + +NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain"; +NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; + +// Returns number of bytes consumed. Returning 0 means you didn't match. +// Sends bytes to callback handler; +typedef size_t (^stream_scanner)(NSData *collected_data); + +typedef void (^data_callback)(SRWebSocket *webSocket, NSData *data); + +@interface SRIOConsumer : NSObject { + stream_scanner _scanner; + data_callback _handler; + size_t _bytesNeeded; + BOOL _readToCurrentFrame; + BOOL _unmaskBytes; +} +@property (nonatomic, copy, readonly) stream_scanner consumer; +@property (nonatomic, copy, readonly) data_callback handler; +@property (nonatomic, assign) size_t bytesNeeded; +@property (nonatomic, assign, readonly) BOOL readToCurrentFrame; +@property (nonatomic, assign, readonly) BOOL unmaskBytes; + +@end + +// This class is not thread-safe, and is expected to always be run on the same queue. +@interface SRIOConsumerPool : NSObject + +- (id)initWithBufferCapacity:(NSUInteger)poolSize; + +- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +- (void)returnConsumer:(SRIOConsumer *)consumer; + +@end + +@interface SRWebSocket () + +- (void)_writeData:(NSData *)data; +- (void)_closeWithProtocolError:(NSString *)message; +- (void)_failWithError:(NSError *)error; + +- (void)_disconnect; + +- (void)_readFrameNew; +- (void)_readFrameContinue; + +- (void)_pumpScanner; + +- (void)_pumpWriting; + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; + +- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +- (void)_SR_commonInit; + +- (void)_initializeStreams; +- (void)_connect; + +@property (nonatomic) SRReadyState readyState; + +@property (nonatomic) NSOperationQueue *delegateOperationQueue; +@property (nonatomic) dispatch_queue_t delegateDispatchQueue; + +@end + + +@implementation SRWebSocket { + NSInteger _webSocketVersion; + + NSOperationQueue *_delegateOperationQueue; + dispatch_queue_t _delegateDispatchQueue; + + dispatch_queue_t _workQueue; + NSMutableArray *_consumers; + + NSInputStream *_inputStream; + NSOutputStream *_outputStream; + + NSMutableData *_readBuffer; + NSUInteger _readBufferOffset; + + NSMutableData *_outputBuffer; + NSUInteger _outputBufferOffset; + + uint8_t _currentFrameOpcode; + size_t _currentFrameCount; + size_t _readOpCount; + uint32_t _currentStringScanPosition; + NSMutableData *_currentFrameData; + + NSString *_closeReason; + + NSString *_secKey; + + BOOL _pinnedCertFound; + + uint8_t _currentReadMaskKey[4]; + size_t _currentReadMaskOffset; + + BOOL _consumerStopped; + + BOOL _closeWhenFinishedWriting; + BOOL _failed; + + BOOL _secure; + NSURLRequest *_urlRequest; + + CFHTTPMessageRef _receivedHTTPHeaders; + + BOOL _sentClose; + BOOL _didFail; + int _closeCode; + + BOOL _isPumping; + + NSMutableSet *_scheduledRunloops; + + // We use this to retain ourselves. + __strong SRWebSocket *_selfRetain; + + NSArray *_requestedProtocols; + SRIOConsumerPool *_consumerPool; +} + +@synthesize delegate = _delegate; +@synthesize url = _url; +@synthesize readyState = _readyState; +@synthesize protocol = _protocol; + +static __strong NSData *CRLFCRLF; + ++ (void)initialize; +{ + CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; +} + +- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +{ + self = [super init]; + if (self) { + assert(request.URL); + _url = request.URL; + _urlRequest = request; + + _requestedProtocols = [protocols copy]; + + [self _SR_commonInit]; + } + + return self; +} + +- (id)initWithURLRequest:(NSURLRequest *)request; +{ + return [self initWithURLRequest:request protocols:nil]; +} + +- (id)initWithURL:(NSURL *)url; +{ + return [self initWithURL:url protocols:nil]; +} + +- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +{ + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + return [self initWithURLRequest:request protocols:protocols]; +} + +- (void)_SR_commonInit; +{ + + NSString *scheme = _url.scheme.lowercaseString; + assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); + + if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { + _secure = YES; + } + + _readyState = SR_CONNECTING; + _consumerStopped = YES; + _webSocketVersion = 13; + + _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + + // Going to set a specific on the queue so we can validate we're on the work queue + dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL); + + _delegateDispatchQueue = dispatch_get_main_queue(); + sr_dispatch_retain(_delegateDispatchQueue); + + _readBuffer = [[NSMutableData alloc] init]; + _outputBuffer = [[NSMutableData alloc] init]; + + _currentFrameData = [[NSMutableData alloc] init]; + + _consumers = [[NSMutableArray alloc] init]; + + _consumerPool = [[SRIOConsumerPool alloc] init]; + + _scheduledRunloops = [[NSMutableSet alloc] init]; + + [self _initializeStreams]; + + // default handlers +} + +- (void)assertOnWorkQueue; +{ + assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue)); +} + +- (void)dealloc +{ + _inputStream.delegate = nil; + _outputStream.delegate = nil; + + [_inputStream close]; + [_outputStream close]; + + sr_dispatch_release(_workQueue); + _workQueue = NULL; + + if (_receivedHTTPHeaders) { + CFRelease(_receivedHTTPHeaders); + _receivedHTTPHeaders = NULL; + } + + if (_delegateDispatchQueue) { + sr_dispatch_release(_delegateDispatchQueue); + _delegateDispatchQueue = NULL; + } +} + +#ifndef NDEBUG + +- (void)setReadyState:(SRReadyState)aReadyState; +{ + [self willChangeValueForKey:@"readyState"]; + assert(aReadyState > _readyState); + _readyState = aReadyState; + [self didChangeValueForKey:@"readyState"]; +} + +#endif + +- (void)open; +{ + assert(_url); + NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once"); + + _selfRetain = self; + + [self _connect]; +} + +// Calls block on delegate queue +- (void)_performDelegateBlock:(dispatch_block_t)block; +{ + if (_delegateOperationQueue) { + [_delegateOperationQueue addOperationWithBlock:block]; + } else { + assert(_delegateDispatchQueue); + dispatch_async(_delegateDispatchQueue, block); + } +} + +- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; +{ + if (queue) { + sr_dispatch_retain(queue); + } + + if (_delegateDispatchQueue) { + sr_dispatch_release(_delegateDispatchQueue); + } + + _delegateDispatchQueue = queue; +} + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +{ + NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); + + if (acceptHeader == nil) { + return NO; + } + + NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString]; + NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding]; + + return [acceptHeader isEqualToString:expectedAccept]; +} + +- (void)_HTTPHeadersDidFinish; +{ + NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); + + if (responseCode >= 400) { + SRFastLog(@"Request failed with response code %d", responseCode); + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]]; + return; + } + + if(![self _checkHandshake:_receivedHTTPHeaders]) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]]; + return; + } + + NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); + if (negotiatedProtocol) { + // Make sure we requested the protocol + if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]]; + return; + } + + _protocol = negotiatedProtocol; + } + + self.readyState = SR_OPEN; + + if (!_didFail) { + [self _readFrameNew]; + } + + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) { + [self.delegate webSocketDidOpen:self]; + }; + }]; +} + + +- (void)_readHTTPHeader; +{ + if (_receivedHTTPHeaders == NULL) { + _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); + } + + [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self, NSData *data) { + CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); + + if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { + SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); + [self _HTTPHeadersDidFinish]; + } else { + [self _readHTTPHeader]; + } + }]; +} + +- (void)didConnect +{ + SRFastLog(@"Connected"); + CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); + + // Set host first so it defaults + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); + + NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; + SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); + + if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { + _secKey = [keyBytes base64EncodedStringWithOptions:0]; + } else { + _secKey = [keyBytes base64Encoding]; + } + + assert([_secKey length] == 24); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin); + + if (_requestedProtocols) { + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]); + } + + [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); + }]; + + NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)); + + CFRelease(request); + + [self _writeData:message]; + [self _readHTTPHeader]; +} + +- (void)_initializeStreams; +{ + assert(_url.port.unsignedIntValue <= UINT32_MAX); + uint32_t port = _url.port.unsignedIntValue; + if (port == 0) { + if (!_secure) { + port = 80; + } else { + port = 443; + } + } + NSString *host = _url.host; + + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + + CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); + + _outputStream = CFBridgingRelease(writeStream); + _inputStream = CFBridgingRelease(readStream); + + + if (_secure) { + NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; + + [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; + + // If we're using pinned certs, don't validate the certificate chain + if ([_urlRequest SR_SSLPinnedCertificates].count) { + [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + } + +#if DEBUG + [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + NSLog(@"SocketRocket: In debug mode. Allowing connection to any root cert"); +#endif + + [_outputStream setProperty:SSLOptions + forKey:(__bridge id)kCFStreamPropertySSLSettings]; + } + + _inputStream.delegate = self; + _outputStream.delegate = self; +} + +- (void)_connect; +{ + if (!_scheduledRunloops.count) { + [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; + } + + + [_outputStream open]; + [_inputStream open]; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream scheduleInRunLoop:aRunLoop forMode:mode]; + [_inputStream scheduleInRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops addObject:@[aRunLoop, mode]]; +} + +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream removeFromRunLoop:aRunLoop forMode:mode]; + [_inputStream removeFromRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops removeObject:@[aRunLoop, mode]]; +} + +- (void)close; +{ + [self closeWithCode:SRStatusCodeNormal reason:nil]; +} + +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; +{ + assert(code); + dispatch_async(_workQueue, ^{ + if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) { + return; + } + + BOOL wasConnecting = self.readyState == SR_CONNECTING; + + self.readyState = SR_CLOSING; + + SRFastLog(@"Closing with code %d reason %@", code, reason); + + if (wasConnecting) { + [self _disconnect]; + return; + } + + size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; + NSData *payload = mutablePayload; + + ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code); + + if (reason) { + NSRange remainingRange = {0}; + + NSUInteger usedLength = 0; + + BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange]; + + assert(success); + assert(remainingRange.length == 0); + + if (usedLength != maxMsgSize) { + payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))]; + } + } + + + [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload]; + }); +} + +- (void)_closeWithProtocolError:(NSString *)message; +{ + // Need to shunt this on the _callbackQueue first to see if they received any messages + [self _performDelegateBlock:^{ + [self closeWithCode:SRStatusCodeProtocolError reason:message]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + }]; +} + +- (void)_failWithError:(NSError *)error; +{ + dispatch_async(_workQueue, ^{ + if (self.readyState != SR_CLOSED) { + _failed = YES; + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) { + [self.delegate webSocket:self didFailWithError:error]; + } + }]; + + self.readyState = SR_CLOSED; + _selfRetain = nil; + + SRFastLog(@"Failing with error %@", error.localizedDescription); + + [self _disconnect]; + } + }); +} + +- (void)_writeData:(NSData *)data; +{ + [self assertOnWorkQueue]; + + if (_closeWhenFinishedWriting) { + return; + } + [_outputBuffer appendData:data]; + [self _pumpWriting]; +} + +- (void)send:(id)data; +{ + NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy]; + dispatch_async(_workQueue, ^{ + if ([data isKindOfClass:[NSString class]]) { + [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]]; + } else if ([data isKindOfClass:[NSData class]]) { + [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data]; + } else if (data == nil) { + [self _sendFrameWithOpcode:SROpCodeTextFrame data:data]; + } else { + assert(NO); + } + }); +} + +- (void)sendPing:(NSData *)data; +{ + NSAssert(self.readyState == SR_OPEN, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePing data:data]; + }); +} + +- (void)handlePing:(NSData *)pingData; +{ + // Need to pingpong this off _callbackQueue first to make sure messages happen in order + [self _performDelegateBlock:^{ + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePong data:pingData]; + }); + }]; +} + +- (void)handlePong:(NSData *)pongData; +{ + SRFastLog(@"Received pong"); + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) { + [self.delegate webSocket:self didReceivePong:pongData]; + } + }]; +} + +- (void)_handleMessage:(id)message +{ + SRFastLog(@"Received message"); + [self _performDelegateBlock:^{ + [self.delegate webSocket:self didReceiveMessage:message]; + }]; +} + + +static inline BOOL closeCodeIsValid(int closeCode) { + if (closeCode < 1000) { + return NO; + } + + if (closeCode >= 1000 && closeCode <= 1011) { + if (closeCode == 1004 || + closeCode == 1005 || + closeCode == 1006) { + return NO; + } + return YES; + } + + if (closeCode >= 3000 && closeCode <= 3999) { + return YES; + } + + if (closeCode >= 4000 && closeCode <= 4999) { + return YES; + } + + return NO; +} + +// Note from RFC: +// +// If there is a body, the first two +// bytes of the body MUST be a 2-byte unsigned integer (in network byte +// order) representing a status code with value /code/ defined in +// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8 +// encoded data with value /reason/, the interpretation of which is not +// defined by this specification. + +- (void)handleCloseWithData:(NSData *)data; +{ + size_t dataSize = data.length; + __block uint16_t closeCode = 0; + + SRFastLog(@"Received close frame"); + + if (dataSize == 1) { + // TODO handle error + [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"]; + return; + } else if (dataSize >= 2) { + [data getBytes:&closeCode length:sizeof(closeCode)]; + _closeCode = EndianU16_BtoN(closeCode); + if (!closeCodeIsValid(_closeCode)) { + [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]]; + return; + } + if (dataSize > 2) { + _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding]; + if (!_closeReason) { + [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"]; + return; + } + } + } else { + _closeCode = SRStatusNoStatusReceived; + } + + [self assertOnWorkQueue]; + + if (self.readyState == SR_OPEN) { + [self closeWithCode:1000 reason:nil]; + } + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); +} + +- (void)_disconnect; +{ + [self assertOnWorkQueue]; + SRFastLog(@"Trying to disconnect"); + _closeWhenFinishedWriting = YES; + [self _pumpWriting]; +} + +- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode; +{ + // Check that the current data is valid UTF8 + + BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose); + if (!isControlFrame) { + [self _readFrameNew]; + } else { + dispatch_async(_workQueue, ^{ + [self _readFrameContinue]; + }); + } + + switch (opcode) { + case SROpCodeTextFrame: { + NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; + if (str == nil && frameData) { + [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + + return; + } + [self _handleMessage:str]; + break; + } + case SROpCodeBinaryFrame: + [self _handleMessage:[frameData copy]]; + break; + case SROpCodeConnectionClose: + [self handleCloseWithData:frameData]; + break; + case SROpCodePing: + [self handlePing:frameData]; + break; + case SROpCodePong: + [self handlePong:frameData]; + break; + default: + [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %ld", (long)opcode]]; + // TODO: Handle invalid opcode + break; + } +} + +- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData; +{ + assert(frame_header.opcode != 0); + + if (self.readyState != SR_OPEN) { + return; + } + + + BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose); + + if (isControlFrame && !frame_header.fin) { + [self _closeWithProtocolError:@"Fragmented control frames not allowed"]; + return; + } + + if (isControlFrame && frame_header.payload_length >= 126) { + [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"]; + return; + } + + if (!isControlFrame) { + _currentFrameOpcode = frame_header.opcode; + _currentFrameCount += 1; + } + + if (frame_header.payload_length == 0) { + if (isControlFrame) { + [self _handleFrameWithData:curData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO add assert that opcode is not a control; + [self _readFrameContinue]; + } + } + } else { + assert(frame_header.payload_length <= SIZE_T_MAX); + [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *newData) { + if (isControlFrame) { + [self _handleFrameWithData:newData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO add assert that opcode is not a control; + [self _readFrameContinue]; + } + + } + } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked]; + } +} + +/* From RFC: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ + */ + +static const uint8_t SRFinMask = 0x80; +static const uint8_t SROpCodeMask = 0x0F; +static const uint8_t SRRsvMask = 0x70; +static const uint8_t SRMaskMask = 0x80; +static const uint8_t SRPayloadLenMask = 0x7F; + + +- (void)_readFrameContinue; +{ + assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0)); + + [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) { + __block frame_header header = {0}; + + const uint8_t *headerBuffer = data.bytes; + assert(data.length >= 2); + + if (headerBuffer[0] & SRRsvMask) { + [self _closeWithProtocolError:@"Server used RSV bits"]; + return; + } + + uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]); + + BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose); + + if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) { + [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; + return; + } + + if (receivedOpcode == 0 && self->_currentFrameCount == 0) { + [self _closeWithProtocolError:@"cannot continue a message"]; + return; + } + + header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode; + + header.fin = !!(SRFinMask & headerBuffer[0]); + + + header.masked = !!(SRMaskMask & headerBuffer[1]); + header.payload_length = SRPayloadLenMask & headerBuffer[1]; + + headerBuffer = NULL; + + if (header.masked) { + [self _closeWithProtocolError:@"Client must receive unmasked data"]; + } + + size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0; + + if (header.payload_length == 126) { + extra_bytes_needed += sizeof(uint16_t); + } else if (header.payload_length == 127) { + extra_bytes_needed += sizeof(uint64_t); + } + + if (extra_bytes_needed == 0) { + [self _handleFrameHeader:header curData:self->_currentFrameData]; + } else { + [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) { + size_t mapped_size = data.length; + const void *mapped_buffer = data.bytes; + size_t offset = 0; + + if (header.payload_length == 126) { + assert(mapped_size >= sizeof(uint16_t)); + uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer)); + header.payload_length = newLen; + offset += sizeof(uint16_t); + } else if (header.payload_length == 127) { + assert(mapped_size >= sizeof(uint64_t)); + header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer)); + offset += sizeof(uint64_t); + } else { + assert(header.payload_length < 126 && header.payload_length >= 0); + } + + + if (header.masked) { + assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); + memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey)); + } + + [self _handleFrameHeader:header curData:self->_currentFrameData]; + } readToCurrentFrame:NO unmaskBytes:NO]; + } + } readToCurrentFrame:NO unmaskBytes:NO]; +} + +- (void)_readFrameNew; +{ + dispatch_async(_workQueue, ^{ + [_currentFrameData setLength:0]; + + _currentFrameOpcode = 0; + _currentFrameCount = 0; + _readOpCount = 0; + _currentStringScanPosition = 0; + + [self _readFrameContinue]; + }); +} + +- (void)_pumpWriting; +{ + [self assertOnWorkQueue]; + + NSUInteger dataLength = _outputBuffer.length; + if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { + NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset]; + if (bytesWritten == -1) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]]; + return; + } + + _outputBufferOffset += bytesWritten; + + if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) { + _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset]; + _outputBufferOffset = 0; + } + } + + if (_closeWhenFinishedWriting && + _outputBuffer.length - _outputBufferOffset == 0 && + (_inputStream.streamStatus != NSStreamStatusNotOpen && + _inputStream.streamStatus != NSStreamStatusClosed) && + !_sentClose) { + _sentClose = YES; + + [_outputStream close]; + [_inputStream close]; + + + for (NSArray *runLoop in [_scheduledRunloops copy]) { + [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]]; + } + + if (!_failed) { + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; + } + }]; + } + + _selfRetain = nil; + } +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +{ + [self assertOnWorkQueue]; + [self _addConsumerWithScanner:consumer callback:callback dataLength:0]; +} + +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + [self assertOnWorkQueue]; + assert(dataLength); + + [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]]; + [self _pumpScanner]; +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +{ + [self assertOnWorkQueue]; + [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]]; + [self _pumpScanner]; +} + + +static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'}; + +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; +{ + [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler]; +} + +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +{ + // TODO optimize so this can continue from where we last searched + stream_scanner consumer = ^size_t(NSData *data) { + __block size_t found_size = 0; + __block size_t match_count = 0; + + size_t size = data.length; + const unsigned char *buffer = data.bytes; + for (size_t i = 0; i < size; i++ ) { + if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) { + match_count += 1; + if (match_count == length) { + found_size = i + 1; + break; + } + } else { + match_count = 0; + } + } + return found_size; + }; + [self _addConsumerWithScanner:consumer callback:dataHandler]; +} + + +// Returns true if did work +- (BOOL)_innerPumpScanner { + + BOOL didWork = NO; + + if (self.readyState >= SR_CLOSING) { + return didWork; + } + + if (!_consumers.count) { + return didWork; + } + + size_t curSize = _readBuffer.length - _readBufferOffset; + if (!curSize) { + return didWork; + } + + SRIOConsumer *consumer = [_consumers objectAtIndex:0]; + + size_t bytesNeeded = consumer.bytesNeeded; + + size_t foundSize = 0; + if (consumer.consumer) { + NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO]; + foundSize = consumer.consumer(tempView); + } else { + assert(consumer.bytesNeeded); + if (curSize >= bytesNeeded) { + foundSize = bytesNeeded; + } else if (consumer.readToCurrentFrame) { + foundSize = curSize; + } + } + + NSData *slice = nil; + if (consumer.readToCurrentFrame || foundSize) { + NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize); + slice = [_readBuffer subdataWithRange:sliceRange]; + + _readBufferOffset += foundSize; + + if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) { + _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0; + } + + if (consumer.unmaskBytes) { + NSMutableData *mutableSlice = [slice mutableCopy]; + + NSUInteger len = mutableSlice.length; + uint8_t *bytes = mutableSlice.mutableBytes; + + for (NSUInteger i = 0; i < len; i++) { + bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)]; + _currentReadMaskOffset += 1; + } + + slice = mutableSlice; + } + + if (consumer.readToCurrentFrame) { + [_currentFrameData appendData:slice]; + + _readOpCount += 1; + + if (_currentFrameOpcode == SROpCodeTextFrame) { + // Validate UTF8 stuff. + size_t currentDataSize = _currentFrameData.length; + if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) { + // TODO: Optimize the crap out of this. Don't really have to copy all the data each time + + size_t scanSize = currentDataSize - _currentStringScanPosition; + + NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)]; + int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data); + + if (valid_utf8_size == -1) { + [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + return didWork; + } else { + _currentStringScanPosition += valid_utf8_size; + } + } + + } + + consumer.bytesNeeded -= foundSize; + + if (consumer.bytesNeeded == 0) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, nil); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } else if (foundSize) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, slice); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } + return didWork; +} + +-(void)_pumpScanner; +{ + [self assertOnWorkQueue]; + + if (!_isPumping) { + _isPumping = YES; + } else { + return; + } + + while ([self _innerPumpScanner]) { + + } + + _isPumping = NO; +} + +//#define NOMASK + +static const size_t SRFrameHeaderOverhead = 32; + +- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; +{ + [self assertOnWorkQueue]; + + if (nil == data) { + return; + } + + NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData"); + + size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; + + NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead]; + if (!frame) { + [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"]; + return; + } + uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; + + // set fin + frame_buffer[0] = SRFinMask | opcode; + + BOOL useMask = YES; +#ifdef NOMASK + useMask = NO; +#endif + + if (useMask) { + // set the mask and header + frame_buffer[1] |= SRMaskMask; + } + + size_t frame_buffer_size = 2; + + const uint8_t *unmasked_payload = NULL; + if ([data isKindOfClass:[NSData class]]) { + unmasked_payload = (uint8_t *)[data bytes]; + } else if ([data isKindOfClass:[NSString class]]) { + unmasked_payload = (const uint8_t *)[data UTF8String]; + } else { + return; + } + + if (payloadLength < 126) { + frame_buffer[1] |= payloadLength; + } else if (payloadLength <= UINT16_MAX) { + frame_buffer[1] |= 126; + *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength); + frame_buffer_size += sizeof(uint16_t); + } else { + frame_buffer[1] |= 127; + *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength); + frame_buffer_size += sizeof(uint64_t); + } + + if (!useMask) { + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i]; + frame_buffer_size += 1; + } + } else { + uint8_t *mask_key = frame_buffer + frame_buffer_size; + SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key); + frame_buffer_size += sizeof(uint32_t); + + // TODO: could probably optimize this with SIMD + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)]; + frame_buffer_size += 1; + } + } + + assert(frame_buffer_size <= [frame length]); + frame.length = frame_buffer_size; + + [self _writeData:frame]; +} + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; +{ + if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { + + NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates]; + if (sslCerts) { + SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; + if (secTrust) { + NSInteger numCerts = SecTrustGetCertificateCount(secTrust); + for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) { + SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i); + NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert)); + + for (id ref in sslCerts) { + SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; + NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); + + if ([trustedCertData isEqualToData:certData]) { + _pinnedCertFound = YES; + break; + } + } + } + } + + if (!_pinnedCertFound) { + dispatch_async(_workQueue, ^{ + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:23556 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid server cert"] forKey:NSLocalizedDescriptionKey]]]; + }); + return; + } + } + } + + dispatch_async(_workQueue, ^{ + switch (eventCode) { + case NSStreamEventOpenCompleted: { + SRFastLog(@"NSStreamEventOpenCompleted %@", aStream); + if (self.readyState >= SR_CLOSING) { + return; + } + assert(_readBuffer); + + if (self.readyState == SR_CONNECTING && aStream == _inputStream) { + [self didConnect]; + } + [self _pumpWriting]; + [self _pumpScanner]; + break; + } + + case NSStreamEventErrorOccurred: { + SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); + /// TODO specify error better! + [self _failWithError:aStream.streamError]; + _readBufferOffset = 0; + [_readBuffer setLength:0]; + break; + + } + + case NSStreamEventEndEncountered: { + [self _pumpScanner]; + SRFastLog(@"NSStreamEventEndEncountered %@", aStream); + if (aStream.streamError) { + [self _failWithError:aStream.streamError]; + } else { + if (self.readyState != SR_CLOSED) { + self.readyState = SR_CLOSED; + _selfRetain = nil; + } + + if (!_sentClose && !_failed) { + _sentClose = YES; + // If we get closed in this state it's probably not clean because we should be sending this when we send messages + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:SRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; + } + }]; + } + } + + break; + } + + case NSStreamEventHasBytesAvailable: { + SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream); + const int bufferSize = 2048; + uint8_t buffer[bufferSize]; + + while (_inputStream.hasBytesAvailable) { + NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize]; + + if (bytes_read > 0) { + [_readBuffer appendBytes:buffer length:bytes_read]; + } else if (bytes_read < 0) { + [self _failWithError:_inputStream.streamError]; + } + + if (bytes_read != bufferSize) { + break; + } + }; + [self _pumpScanner]; + break; + } + + case NSStreamEventHasSpaceAvailable: { + SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream); + [self _pumpWriting]; + break; + } + + default: + SRFastLog(@"(default) %@", aStream); + break; + } + }); +} + +@end + + +@implementation SRIOConsumer + +@synthesize bytesNeeded = _bytesNeeded; +@synthesize consumer = _scanner; +@synthesize handler = _handler; +@synthesize readToCurrentFrame = _readToCurrentFrame; +@synthesize unmaskBytes = _unmaskBytes; + +- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + _scanner = [scanner copy]; + _handler = [handler copy]; + _bytesNeeded = bytesNeeded; + _readToCurrentFrame = readToCurrentFrame; + _unmaskBytes = unmaskBytes; + assert(_scanner || _bytesNeeded); +} + + +@end + + +@implementation SRIOConsumerPool { + NSUInteger _poolSize; + NSMutableArray *_bufferedConsumers; +} + +- (id)initWithBufferCapacity:(NSUInteger)poolSize; +{ + self = [super init]; + if (self) { + _poolSize = poolSize; + _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize]; + } + return self; +} + +- (id)init +{ + return [self initWithBufferCapacity:8]; +} + +- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + SRIOConsumer *consumer = nil; + if (_bufferedConsumers.count) { + consumer = [_bufferedConsumers lastObject]; + [_bufferedConsumers removeLastObject]; + } else { + consumer = [[SRIOConsumer alloc] init]; + } + + [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; + + return consumer; +} + +- (void)returnConsumer:(SRIOConsumer *)consumer; +{ + if (_bufferedConsumers.count < _poolSize) { + [_bufferedConsumers addObject:consumer]; + } +} + +@end + + +@implementation NSURLRequest (CertificateAdditions) + +- (NSArray *)SR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSMutableURLRequest (CertificateAdditions) + +- (NSArray *)SR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +- (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates; +{ + [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSURL (SRWebSocket) + +- (NSString *)SR_origin; +{ + NSString *scheme = [self.scheme lowercaseString]; + + if ([scheme isEqualToString:@"wss"]) { + scheme = @"https"; + } else if ([scheme isEqualToString:@"ws"]) { + scheme = @"http"; + } + + if (self.port) { + return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; + } else { + return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; + } +} + +@end + +//#define SR_ENABLE_LOG + +static inline void SRFastLog(NSString *format, ...) { +#ifdef SR_ENABLE_LOG + __block va_list arg_list; + va_start (arg_list, format); + + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + + va_end(arg_list); + + NSLog(@"[SR] %@", formattedString); +#endif +} + + +#ifdef HAS_ICU + +static inline int32_t validate_dispatch_data_partial_string(NSData *data) { + if ([data length] > INT32_MAX) { + // INT32_MAX is the limit so long as this Framework is using 32 bit ints everywhere. + return -1; + } + + int32_t size = (int32_t)[data length]; + + const void * contents = [data bytes]; + const uint8_t *str = (const uint8_t *)contents; + + UChar32 codepoint = 1; + int32_t offset = 0; + int32_t lastOffset = 0; + while(offset < size && codepoint > 0) { + lastOffset = offset; + U8_NEXT(str, offset, size, codepoint); + } + + if (codepoint == -1) { + // Check to see if the last byte is valid or whether it was just continuing + if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) { + + size = -1; + } else { + uint8_t leadByte = str[lastOffset]; + U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte)); + + for (int i = lastOffset + 1; i < offset; i++) { + if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) { + size = -1; + } + } + + if (size != -1) { + size = lastOffset; + } + } + } + + if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) { + size = -1; + } + + return size; +} + +#else + +// This is a hack, and probably not optimal +static inline int32_t validate_dispatch_data_partial_string(NSData *data) { + static const int maxCodepointSize = 3; + + for (int i = 0; i < maxCodepointSize; i++) { + NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO]; + if (str) { + return data.length - i; + } + } + + return -1; +} + +#endif + +static _SRRunLoopThread *networkThread = nil; +static NSRunLoop *networkRunLoop = nil; + +@implementation NSRunLoop (SRWebSocket) + ++ (NSRunLoop *)SR_networkRunLoop { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + networkThread = [[_SRRunLoopThread alloc] init]; + networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; + [networkThread start]; + networkRunLoop = networkThread.runLoop; + }); + + return networkRunLoop; +} + +@end + + +@implementation _SRRunLoopThread { + dispatch_group_t _waitGroup; +} + +@synthesize runLoop = _runLoop; + +- (void)dealloc +{ + sr_dispatch_release(_waitGroup); +} + +- (id)init +{ + self = [super init]; + if (self) { + _waitGroup = dispatch_group_create(); + dispatch_group_enter(_waitGroup); + } + return self; +} + +- (void)main; +{ + @autoreleasepool { + _runLoop = [NSRunLoop currentRunLoop]; + dispatch_group_leave(_waitGroup); + + NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO]; + [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode]; + + while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { + + } + assert(NO); + } +} + +- (NSRunLoop *)runLoop; +{ + dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); + return _runLoop; +} + +@end diff --git a/talk/libjingle_examples.gyp b/talk/libjingle_examples.gyp index f17fbfe7a4..f2c122baf1 100755 --- a/talk/libjingle_examples.gyp +++ b/talk/libjingle_examples.gyp @@ -226,11 +226,9 @@ 'type': 'executable', 'product_name': 'AppRTCDemo', 'mac_bundle': 1, - 'mac_bundle_resources': [ - 'examples/objc/AppRTCDemo/channel.html', - ], 'dependencies': [ 'libjingle.gyp:libjingle_peerconnection_objc', + 'socketrocket', ], 'conditions': [ ['OS=="ios"', { @@ -265,7 +263,6 @@ 'MACOSX_DEPLOYMENT_TARGET' : '10.8', 'OTHER_LDFLAGS': [ '-framework AVFoundation', - '-framework WebKit', ], }, }], @@ -279,16 +276,18 @@ 'examples/objc/APPRTCDemo', ], 'sources': [ - 'examples/objc/AppRTCDemo/APPRTCAppClient.h', - 'examples/objc/AppRTCDemo/APPRTCAppClient.m', - 'examples/objc/AppRTCDemo/APPRTCConnectionManager.h', - 'examples/objc/AppRTCDemo/APPRTCConnectionManager.m', - 'examples/objc/AppRTCDemo/ARDSignalingParams.h', - 'examples/objc/AppRTCDemo/ARDSignalingParams.m', + 'examples/objc/AppRTCDemo/ARDAppClient.h', + 'examples/objc/AppRTCDemo/ARDAppClient.m', + 'examples/objc/AppRTCDemo/ARDMessageResponse.h', + 'examples/objc/AppRTCDemo/ARDMessageResponse.m', + 'examples/objc/AppRTCDemo/ARDRegisterResponse.h', + 'examples/objc/AppRTCDemo/ARDRegisterResponse.m', + 'examples/objc/AppRTCDemo/ARDSignalingMessage.h', + 'examples/objc/AppRTCDemo/ARDSignalingMessage.m', 'examples/objc/AppRTCDemo/ARDUtilities.h', 'examples/objc/AppRTCDemo/ARDUtilities.m', - 'examples/objc/AppRTCDemo/GAEChannelClient.h', - 'examples/objc/AppRTCDemo/GAEChannelClient.m', + 'examples/objc/AppRTCDemo/ARDWebSocketChannel.h', + 'examples/objc/AppRTCDemo/ARDWebSocketChannel.m', 'examples/objc/AppRTCDemo/RTCICECandidate+JSON.h', 'examples/objc/AppRTCDemo/RTCICECandidate+JSON.m', 'examples/objc/AppRTCDemo/RTCICEServer+JSON.h', @@ -302,6 +301,47 @@ 'CLANG_ENABLE_OBJC_ARC': 'YES', }, }, # target AppRTCDemo + { + # TODO(tkchin): move this into the real third party location and + # have it mirrored on chrome infra. + 'target_name': 'socketrocket', + 'type': 'static_library', + 'sources': [ + 'examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.h', + 'examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.m', + ], + 'conditions': [ + ['OS=="mac"', { + 'xcode_settings': { + # SocketRocket autosynthesizes some properties. Disable the + # warning so we can compile successfully. + 'CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS': 'NO', + 'MACOSX_DEPLOYMENT_TARGET' : '10.8', + }, + }], + ], + 'direct_dependent_settings': { + 'include_dirs': [ + 'examples/objc/AppRTCDemo/third_party/SocketRocket', + ], + }, + 'xcode_settings': { + 'CLANG_ENABLE_OBJC_ARC': 'YES', + 'WARNING_CFLAGS': [ + '-Wno-deprecated-declarations', + ], + }, + 'link_settings': { + 'xcode_settings': { + 'OTHER_LDFLAGS': [ + '-framework CFNetwork', + ], + }, + 'libraries': [ + '$(SDKROOT)/usr/lib/libicucore.dylib', + ], + } + }, # target socketrocket ], # targets }], # OS=="ios" or (OS=="mac" and target_arch!="ia32" and mac_sdk>="10.8")