| /* |
| * 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 "APPRTCAppClient.h" |
| |
| #import <dispatch/dispatch.h> |
| |
| #import "GAEChannelClient.h" |
| #import "RTCICEServer.h" |
| #import "APPRTCAppDelegate.h" |
| #import "RTCMediaConstraints.h" |
| |
| @interface APPRTCAppClient () |
| |
| @property(nonatomic, strong) dispatch_queue_t backgroundQueue; |
| @property(nonatomic, copy) NSString* baseURL; |
| @property(nonatomic, strong) GAEChannelClient* gaeChannel; |
| @property(nonatomic, copy) NSString* postMessageUrl; |
| @property(nonatomic, copy) NSString* pcConfig; |
| @property(nonatomic, strong) NSMutableString* roomHtml; |
| @property(atomic, strong) NSMutableArray* sendQueue; |
| @property(nonatomic, copy) NSString* token; |
| |
| @property(nonatomic, assign) BOOL verboseLogging; |
| |
| @end |
| |
| @implementation APPRTCAppClient |
| |
| - (id)initWithICEServerDelegate:(id<ICEServerDelegate>)delegate |
| messageHandler:(id<GAEMessageHandler>)handler { |
| if (self = [super init]) { |
| _ICEServerDelegate = delegate; |
| _messageHandler = handler; |
| _backgroundQueue = dispatch_queue_create("RTCBackgroundQueue", |
| DISPATCH_QUEUE_SERIAL); |
| _sendQueue = [NSMutableArray array]; |
| // Uncomment to see Request/Response logging. |
| // _verboseLogging = YES; |
| } |
| return self; |
| } |
| |
| #pragma mark - Public methods |
| |
| - (void)connectToRoom:(NSURL*)url { |
| NSURLRequest* request = [self getRequestFromUrl:url]; |
| [NSURLConnection connectionWithRequest:request delegate:self]; |
| } |
| |
| - (void)sendData:(NSData*)data { |
| [self maybeLogMessage:@"Send message"]; |
| |
| dispatch_async(self.backgroundQueue, ^{ |
| [self.sendQueue addObject:[data copy]]; |
| |
| if ([self.postMessageUrl length] < 1) { |
| return; |
| } |
| for (NSData* data in self.sendQueue) { |
| NSString* url = |
| [NSString stringWithFormat:@"%@/%@", |
| self.baseURL, self.postMessageUrl]; |
| [self sendData:data withUrl:url]; |
| } |
| [self.sendQueue removeAllObjects]; |
| }); |
| } |
| |
| #pragma mark - Internal methods |
| |
| - (NSString*)findVar:(NSString*)name strippingQuotes:(BOOL)strippingQuotes { |
| NSError* error; |
| NSString* pattern = |
| [NSString stringWithFormat:@".*\n *var %@ = ([^\n]*);\n.*", name]; |
| NSRegularExpression* regexp = |
| [NSRegularExpression regularExpressionWithPattern:pattern |
| options:0 |
| error:&error]; |
| NSAssert(!error, |
| @"Unexpected error compiling regex: ", |
| error.localizedDescription); |
| |
| NSRange fullRange = NSMakeRange(0, [self.roomHtml length]); |
| NSArray* matches = |
| [regexp matchesInString:self.roomHtml options:0 range:fullRange]; |
| if ([matches count] != 1) { |
| [self showMessage:[NSString stringWithFormat:@"%d matches for %@ in %@", |
| [matches count], |
| name, |
| self.roomHtml]]; |
| return nil; |
| } |
| NSRange matchRange = [matches[0] rangeAtIndex:1]; |
| NSString* value = [self.roomHtml substringWithRange:matchRange]; |
| if (strippingQuotes) { |
| NSAssert([value length] > 2, |
| @"Can't strip quotes from short string: [%@]", |
| value); |
| NSAssert(([value characterAtIndex:0] == '\'' && |
| [value characterAtIndex:[value length] - 1] == '\''), |
| @"Can't strip quotes from unquoted string: [%@]", |
| value); |
| value = [value substringWithRange:NSMakeRange(1, [value length] - 2)]; |
| } |
| return value; |
| } |
| |
| - (NSURLRequest*)getRequestFromUrl:(NSURL*)url { |
| self.roomHtml = [NSMutableString stringWithCapacity:20000]; |
| NSString* path = |
| [NSString stringWithFormat:@"https:%@", [url resourceSpecifier]]; |
| NSURLRequest* request = |
| [NSURLRequest requestWithURL:[NSURL URLWithString:path]]; |
| return request; |
| } |
| |
| - (void)maybeLogMessage:(NSString*)message { |
| if (self.verboseLogging) { |
| NSLog(@"%@", message); |
| } |
| } |
| |
| - (void)sendData:(NSData*)data withUrl:(NSString*)url { |
| NSMutableURLRequest* request = |
| [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; |
| request.HTTPMethod = @"POST"; |
| [request setHTTPBody:data]; |
| NSURLResponse* response; |
| NSError* error; |
| NSData* responseData = [NSURLConnection sendSynchronousRequest:request |
| returningResponse:&response |
| error:&error]; |
| NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; |
| int status = [httpResponse statusCode]; |
| NSAssert(status == 200, |
| @"Bad response [%d] to message: %@\n\n%@", |
| status, |
| [NSString stringWithUTF8String:[data bytes]], |
| [NSString stringWithUTF8String:[responseData bytes]]); |
| } |
| |
| - (void)showMessage:(NSString*)message { |
| NSLog(@"%@", message); |
| UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Unable to join" |
| message:message |
| delegate:nil |
| cancelButtonTitle:@"OK" |
| otherButtonTitles:nil]; |
| [alertView show]; |
| } |
| |
| - (void)updateICEServers:(NSMutableArray*)ICEServers |
| withTurnServer:(NSString*)turnServerUrl { |
| if ([turnServerUrl length] < 1) { |
| [self.ICEServerDelegate onICEServers:ICEServers]; |
| return; |
| } |
| dispatch_async(self.backgroundQueue, ^(void) { |
| NSMutableURLRequest* request = [NSMutableURLRequest |
| requestWithURL:[NSURL URLWithString:turnServerUrl]]; |
| [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"]; |
| [request addValue:@"https://apprtc.appspot.com" |
| forHTTPHeaderField:@"origin"]; |
| NSURLResponse* response; |
| NSError* error; |
| NSData* responseData = [NSURLConnection sendSynchronousRequest:request |
| returningResponse:&response |
| error:&error]; |
| if (!error) { |
| NSDictionary* json = |
| [NSJSONSerialization JSONObjectWithData:responseData |
| options:0 |
| error:&error]; |
| NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); |
| NSString* username = json[@"username"]; |
| NSString* password = json[@"password"]; |
| NSArray* uris = json[@"uris"]; |
| for (int i = 0; i < [uris count]; ++i) { |
| NSString* turnServer = [uris objectAtIndex:i]; |
| RTCICEServer* ICEServer = |
| [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:turnServer] |
| username:username |
| password:password]; |
| NSLog(@"Added ICE Server: %@", ICEServer); |
| [ICEServers addObject:ICEServer]; |
| } |
| } else { |
| NSLog(@"Unable to get TURN server. Error: %@", error.description); |
| } |
| |
| dispatch_async(dispatch_get_main_queue(), ^(void) { |
| [self.ICEServerDelegate onICEServers:ICEServers]; |
| }); |
| }); |
| } |
| |
| #pragma mark - NSURLConnectionDataDelegate methods |
| |
| - (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data { |
| NSString* roomHtml = [NSString stringWithUTF8String:[data bytes]]; |
| [self maybeLogMessage:[NSString stringWithFormat:@"Received %d chars", |
| [roomHtml length]]]; |
| [self.roomHtml appendString:roomHtml]; |
| } |
| |
| - (void)connection:(NSURLConnection*)connection |
| didReceiveResponse:(NSURLResponse*)response { |
| NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; |
| int statusCode = [httpResponse statusCode]; |
| [self |
| maybeLogMessage: |
| [NSString stringWithFormat: |
| @"Response received\nURL\n%@\nStatus [%d]\nHeaders\n%@", |
| [httpResponse URL], |
| statusCode, |
| [httpResponse allHeaderFields]]]; |
| NSAssert(statusCode == 200, @"Invalid response of %d received.", statusCode); |
| } |
| |
| - (void)connectionDidFinishLoading:(NSURLConnection*)connection { |
| [self maybeLogMessage:[NSString stringWithFormat:@"finished loading %d chars", |
| [self.roomHtml length]]]; |
| NSRegularExpression* fullRegex = |
| [NSRegularExpression regularExpressionWithPattern:@"room is full" |
| options:0 |
| error:nil]; |
| if ([fullRegex |
| numberOfMatchesInString:self.roomHtml |
| options:0 |
| range:NSMakeRange(0, [self.roomHtml length])]) { |
| [self showMessage:@"Room full"]; |
| APPRTCAppDelegate* ad = |
| (APPRTCAppDelegate*)[[UIApplication sharedApplication] delegate]; |
| [ad closeVideoUI]; |
| return; |
| } |
| |
| NSString* fullUrl = [[[connection originalRequest] URL] absoluteString]; |
| NSRange queryRange = [fullUrl rangeOfString:@"?"]; |
| self.baseURL = [fullUrl substringToIndex:queryRange.location]; |
| [self maybeLogMessage:[NSString |
| stringWithFormat:@"Base URL: %@", self.baseURL]]; |
| |
| self.initiator = [[self findVar:@"initiator" strippingQuotes:NO] boolValue]; |
| self.token = [self findVar:@"channelToken" strippingQuotes:YES]; |
| if (!self.token) |
| return; |
| [self maybeLogMessage:[NSString stringWithFormat:@"Token: %@", self.token]]; |
| |
| NSString* roomKey = [self findVar:@"roomKey" strippingQuotes:YES]; |
| NSString* me = [self findVar:@"me" strippingQuotes:YES]; |
| if (!roomKey || !me) |
| return; |
| self.postMessageUrl = |
| [NSString stringWithFormat:@"/message?r=%@&u=%@", roomKey, me]; |
| [self maybeLogMessage:[NSString stringWithFormat:@"POST message URL: %@", |
| self.postMessageUrl]]; |
| |
| NSString* pcConfig = [self findVar:@"pcConfig" strippingQuotes:NO]; |
| if (!pcConfig) |
| return; |
| [self maybeLogMessage:[NSString |
| stringWithFormat:@"PC Config JSON: %@", pcConfig]]; |
| |
| NSString* turnServerUrl = [self findVar:@"turnUrl" strippingQuotes:YES]; |
| if (turnServerUrl) { |
| [self maybeLogMessage:[NSString |
| stringWithFormat:@"TURN server request URL: %@", |
| turnServerUrl]]; |
| } |
| |
| NSError* error; |
| NSData* pcData = [pcConfig dataUsingEncoding:NSUTF8StringEncoding]; |
| NSDictionary* json = |
| [NSJSONSerialization JSONObjectWithData:pcData options:0 error:&error]; |
| NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); |
| NSArray* servers = [json objectForKey:@"iceServers"]; |
| NSMutableArray* ICEServers = [NSMutableArray array]; |
| for (NSDictionary* server in servers) { |
| NSString* url = [server objectForKey:@"urls"]; |
| NSString* username = json[@"username"]; |
| NSString* credential = [server objectForKey:@"credential"]; |
| if (!username) { |
| username = @""; |
| } |
| if (!credential) { |
| credential = @""; |
| } |
| [self maybeLogMessage:[NSString |
| stringWithFormat:@"url [%@] - credential [%@]", |
| url, |
| credential]]; |
| RTCICEServer* ICEServer = |
| [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:url] |
| username:username |
| password:credential]; |
| NSLog(@"Added ICE Server: %@", ICEServer); |
| [ICEServers addObject:ICEServer]; |
| } |
| [self updateICEServers:ICEServers withTurnServer:turnServerUrl]; |
| |
| NSString* mc = [self findVar:@"mediaConstraints" strippingQuotes:NO]; |
| if (mc) { |
| error = nil; |
| NSData* mcData = [mc dataUsingEncoding:NSUTF8StringEncoding]; |
| json = |
| [NSJSONSerialization JSONObjectWithData:mcData options:0 error:&error]; |
| NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); |
| if ([[json objectForKey:@"video"] boolValue]) { |
| _videoConstraints = [[RTCMediaConstraints alloc] init]; |
| } |
| } |
| |
| [self |
| maybeLogMessage:[NSString |
| stringWithFormat:@"About to open GAE with token: %@", |
| self.token]]; |
| self.gaeChannel = |
| [[GAEChannelClient alloc] initWithToken:self.token |
| delegate:self.messageHandler]; |
| } |
| |
| @end |