mirror of
https://github.com/mollyim/webrtc.git
synced 2025-05-20 00:57:49 +01:00
AppRTCDemo(iOS): remote-video reliability fixes
Previously GAE Channel callbacks would be handled by JS string-encoding the payload into a URL. Unfortunately this is limited to the (undocumented, silently problematic) maximum URL length UIWebView supports. Replaced this scheme by a notification from JS to ObjC and a getter from ObjC to JS (which happens out-of-line to avoid worrying about UIWebView's re-entrancy, or lack thereof). Part of this change also moved from a combination of: JSON, URL-escaping, and ad-hoc :-separated values to simply JSON. Also incidentally: - Removed outdated TODO about onRenegotiationNeeded, which is unneeded - Move handling of PeerConnection callbacks to the main queue to avoid having to think about concurrency too hard. - Replaced a bunch of NSOrderedSame with isEqualToString for clearer code and not having to worry about the fact that [nil compare:@"foo"]==NSOrderedSame is always true (yay ObjC!). - Auto-scroll messages view. BUG=3117 R=noahric@google.com Review URL: https://webrtc-codereview.appspot.com/10899006 git-svn-id: http://webrtc.googlecode.com/svn/trunk@5814 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
parent
30cd5b5278
commit
61e78fca6c
7 changed files with 168 additions and 143 deletions
|
@ -64,19 +64,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)peerConnectionOnError:(RTCPeerConnection*)peerConnection {
|
- (void)peerConnectionOnError:(RTCPeerConnection*)peerConnection {
|
||||||
NSLog(@"PCO onError.");
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
NSAssert(NO, @"PeerConnection failed.");
|
NSLog(@"PCO onError.");
|
||||||
|
NSAssert(NO, @"PeerConnection failed.");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
signalingStateChanged:(RTCSignalingState)stateChanged {
|
signalingStateChanged:(RTCSignalingState)stateChanged {
|
||||||
NSLog(@"PCO onSignalingStateChange: %d", stateChanged);
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
NSLog(@"PCO onSignalingStateChange: %d", stateChanged);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
addedStream:(RTCMediaStream*)stream {
|
addedStream:(RTCMediaStream*)stream {
|
||||||
NSLog(@"PCO onAddStream.");
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
NSLog(@"PCO onAddStream.");
|
||||||
NSAssert([stream.audioTracks count] <= 1,
|
NSAssert([stream.audioTracks count] <= 1,
|
||||||
@"Expected at most 1 audio stream");
|
@"Expected at most 1 audio stream");
|
||||||
NSAssert([stream.videoTracks count] <= 1,
|
NSAssert([stream.videoTracks count] <= 1,
|
||||||
|
@ -90,49 +94,57 @@
|
||||||
|
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
removedStream:(RTCMediaStream*)stream {
|
removedStream:(RTCMediaStream*)stream {
|
||||||
NSLog(@"PCO onRemoveStream.");
|
dispatch_async(dispatch_get_main_queue(),
|
||||||
|
^(void) { NSLog(@"PCO onRemoveStream."); });
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection*)peerConnection {
|
- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection*)peerConnection {
|
||||||
NSLog(@"PCO onRenegotiationNeeded.");
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
// TODO(hughv): Handle this.
|
NSLog(@"PCO onRenegotiationNeeded - ignoring because AppRTC has a "
|
||||||
|
"predefined negotiation strategy");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
gotICECandidate:(RTCICECandidate*)candidate {
|
gotICECandidate:(RTCICECandidate*)candidate {
|
||||||
NSLog(@"PCO onICECandidate.\n Mid[%@] Index[%d] Sdp[%@]",
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
candidate.sdpMid,
|
NSLog(@"PCO onICECandidate.\n Mid[%@] Index[%d] Sdp[%@]",
|
||||||
candidate.sdpMLineIndex,
|
candidate.sdpMid,
|
||||||
candidate.sdp);
|
candidate.sdpMLineIndex,
|
||||||
NSDictionary* json = @{
|
candidate.sdp);
|
||||||
@"type" : @"candidate",
|
NSDictionary* json = @{
|
||||||
@"label" : [NSNumber numberWithInt:candidate.sdpMLineIndex],
|
@"type" : @"candidate",
|
||||||
@"id" : candidate.sdpMid,
|
@"label" : [NSNumber numberWithInt:candidate.sdpMLineIndex],
|
||||||
@"candidate" : candidate.sdp
|
@"id" : candidate.sdpMid,
|
||||||
};
|
@"candidate" : candidate.sdp
|
||||||
NSError* error;
|
};
|
||||||
NSData* data =
|
NSError* error;
|
||||||
[NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
|
NSData* data =
|
||||||
if (!error) {
|
[NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
|
||||||
[_delegate sendData:data];
|
if (!error) {
|
||||||
} else {
|
[_delegate sendData:data];
|
||||||
NSAssert(NO,
|
} else {
|
||||||
@"Unable to serialize JSON object with error: %@",
|
NSAssert(NO,
|
||||||
error.localizedDescription);
|
@"Unable to serialize JSON object with error: %@",
|
||||||
}
|
error.localizedDescription);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
iceGatheringChanged:(RTCICEGatheringState)newState {
|
iceGatheringChanged:(RTCICEGatheringState)newState {
|
||||||
NSLog(@"PCO onIceGatheringChange. %d", newState);
|
dispatch_async(dispatch_get_main_queue(),
|
||||||
|
^(void) { NSLog(@"PCO onIceGatheringChange. %d", newState); });
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
iceConnectionChanged:(RTCICEConnectionState)newState {
|
iceConnectionChanged:(RTCICEConnectionState)newState {
|
||||||
NSLog(@"PCO onIceConnectionChange. %d", newState);
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
if (newState == RTCICEConnectionConnected)
|
NSLog(@"PCO onIceConnectionChange. %d", newState);
|
||||||
[self displayLogMessage:@"ICE Connection Connected."];
|
if (newState == RTCICEConnectionConnected)
|
||||||
NSAssert(newState != RTCICEConnectionFailed, @"ICE Connection failed!");
|
[self displayLogMessage:@"ICE Connection Connected."];
|
||||||
|
NSAssert(newState != RTCICEConnectionFailed, @"ICE Connection failed!");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)displayLogMessage:(NSString*)message {
|
- (void)displayLogMessage:(NSString*)message {
|
||||||
|
@ -198,6 +210,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)displayLogMessage:(NSString*)message {
|
- (void)displayLogMessage:(NSString*)message {
|
||||||
|
NSAssert([NSThread isMainThread], @"Called off main thread!");
|
||||||
NSLog(@"%@", message);
|
NSLog(@"%@", message);
|
||||||
[self.viewController displayText:message];
|
[self.viewController displayText:message];
|
||||||
}
|
}
|
||||||
|
@ -263,7 +276,7 @@
|
||||||
|
|
||||||
[lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]];
|
[lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]];
|
||||||
[self.peerConnection addStream:lms constraints:constraints];
|
[self.peerConnection addStream:lms constraints:constraints];
|
||||||
[self displayLogMessage:@"onICEServers - add local stream."];
|
[self displayLogMessage:@"onICEServers - added local stream."];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - GAEMessageHandler methods
|
#pragma mark - GAEMessageHandler methods
|
||||||
|
@ -286,24 +299,15 @@
|
||||||
[self displayLogMessage:@"PC - createOffer."];
|
[self displayLogMessage:@"PC - createOffer."];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onMessage:(NSString*)data {
|
- (void)onMessage:(NSDictionary*)messageData {
|
||||||
NSString* message = [self unHTMLifyString:data];
|
NSString* type = messageData[@"type"];
|
||||||
NSError* error;
|
NSAssert(type, @"Missing type: %@", messageData);
|
||||||
NSDictionary* objects = [NSJSONSerialization
|
|
||||||
JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding]
|
|
||||||
options:0
|
|
||||||
error:&error];
|
|
||||||
NSAssert(!error,
|
|
||||||
@"%@",
|
|
||||||
[NSString stringWithFormat:@"Error: %@", error.description]);
|
|
||||||
NSAssert([objects count] > 0, @"Invalid JSON object");
|
|
||||||
NSString* value = [objects objectForKey:@"type"];
|
|
||||||
[self displayLogMessage:[NSString stringWithFormat:@"GAE onMessage type - %@",
|
[self displayLogMessage:[NSString stringWithFormat:@"GAE onMessage type - %@",
|
||||||
value]];
|
type]];
|
||||||
if ([value compare:@"candidate"] == NSOrderedSame) {
|
if ([type isEqualToString:@"candidate"]) {
|
||||||
NSString* mid = [objects objectForKey:@"id"];
|
NSString* mid = messageData[@"id"];
|
||||||
NSNumber* sdpLineIndex = [objects objectForKey:@"label"];
|
NSNumber* sdpLineIndex = messageData[@"label"];
|
||||||
NSString* sdp = [objects objectForKey:@"candidate"];
|
NSString* sdp = messageData[@"candidate"];
|
||||||
RTCICECandidate* candidate =
|
RTCICECandidate* candidate =
|
||||||
[[RTCICECandidate alloc] initWithMid:mid
|
[[RTCICECandidate alloc] initWithMid:mid
|
||||||
index:sdpLineIndex.intValue
|
index:sdpLineIndex.intValue
|
||||||
|
@ -313,16 +317,16 @@
|
||||||
} else {
|
} else {
|
||||||
[self.peerConnection addICECandidate:candidate];
|
[self.peerConnection addICECandidate:candidate];
|
||||||
}
|
}
|
||||||
} else if (([value compare:@"offer"] == NSOrderedSame) ||
|
} else if ([type isEqualToString:@"offer"] ||
|
||||||
([value compare:@"answer"] == NSOrderedSame)) {
|
[type isEqualToString:@"answer"]) {
|
||||||
NSString* sdpString = [objects objectForKey:@"sdp"];
|
NSString* sdpString = messageData[@"sdp"];
|
||||||
RTCSessionDescription* sdp = [[RTCSessionDescription alloc]
|
RTCSessionDescription* sdp = [[RTCSessionDescription alloc]
|
||||||
initWithType:value
|
initWithType:type
|
||||||
sdp:[APPRTCAppDelegate preferISAC:sdpString]];
|
sdp:[APPRTCAppDelegate preferISAC:sdpString]];
|
||||||
[self.peerConnection setRemoteDescriptionWithDelegate:self
|
[self.peerConnection setRemoteDescriptionWithDelegate:self
|
||||||
sessionDescription:sdp];
|
sessionDescription:sdp];
|
||||||
[self displayLogMessage:@"PC - setRemoteDescription."];
|
[self displayLogMessage:@"PC - setRemoteDescription."];
|
||||||
} else if ([value compare:@"bye"] == NSOrderedSame) {
|
} else if ([type isEqualToString:@"bye"]) {
|
||||||
[self closeVideoUI];
|
[self closeVideoUI];
|
||||||
UIAlertView* alertView =
|
UIAlertView* alertView =
|
||||||
[[UIAlertView alloc] initWithTitle:@"Remote end hung up"
|
[[UIAlertView alloc] initWithTitle:@"Remote end hung up"
|
||||||
|
@ -332,7 +336,7 @@
|
||||||
otherButtonTitles:nil];
|
otherButtonTitles:nil];
|
||||||
[alertView show];
|
[alertView show];
|
||||||
} else {
|
} else {
|
||||||
NSAssert(NO, @"Invalid message: %@", data);
|
NSAssert(NO, @"Invalid message: %@", messageData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,8 +346,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onError:(int)code withDescription:(NSString*)description {
|
- (void)onError:(int)code withDescription:(NSString*)description {
|
||||||
[self displayLogMessage:[NSString stringWithFormat:@"GAE onError: %@",
|
[self displayLogMessage:[NSString stringWithFormat:@"GAE onError: %d, %@",
|
||||||
description]];
|
code, description]];
|
||||||
[self closeVideoUI];
|
[self closeVideoUI];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -400,8 +404,8 @@
|
||||||
[newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
|
[newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
|
||||||
[newMLine addObject:isac16kRtpMap];
|
[newMLine addObject:isac16kRtpMap];
|
||||||
for (; origPartIndex < [origMLineParts count]; ++origPartIndex) {
|
for (; origPartIndex < [origMLineParts count]; ++origPartIndex) {
|
||||||
if ([isac16kRtpMap compare:[origMLineParts objectAtIndex:origPartIndex]] !=
|
if (![isac16kRtpMap
|
||||||
NSOrderedSame) {
|
isEqualToString:[origMLineParts objectAtIndex:origPartIndex]]) {
|
||||||
[newMLine addObject:[origMLineParts objectAtIndex:origPartIndex]];
|
[newMLine addObject:[origMLineParts objectAtIndex:origPartIndex]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -415,20 +419,20 @@
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
didCreateSessionDescription:(RTCSessionDescription*)origSdp
|
didCreateSessionDescription:(RTCSessionDescription*)origSdp
|
||||||
error:(NSError*)error {
|
error:(NSError*)error {
|
||||||
if (error) {
|
|
||||||
[self displayLogMessage:@"SDP onFailure."];
|
|
||||||
NSAssert(NO, error.description);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self displayLogMessage:@"SDP onSuccess(SDP) - set local description."];
|
|
||||||
RTCSessionDescription* sdp = [[RTCSessionDescription alloc]
|
|
||||||
initWithType:origSdp.type
|
|
||||||
sdp:[APPRTCAppDelegate preferISAC:origSdp.description]];
|
|
||||||
[self.peerConnection setLocalDescriptionWithDelegate:self
|
|
||||||
sessionDescription:sdp];
|
|
||||||
[self displayLogMessage:@"PC setLocalDescription."];
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
if (error) {
|
||||||
|
[self displayLogMessage:@"SDP onFailure."];
|
||||||
|
NSAssert(NO, error.description);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self displayLogMessage:@"SDP onSuccess(SDP) - set local description."];
|
||||||
|
RTCSessionDescription* sdp = [[RTCSessionDescription alloc]
|
||||||
|
initWithType:origSdp.type
|
||||||
|
sdp:[APPRTCAppDelegate preferISAC:origSdp.description]];
|
||||||
|
[self.peerConnection setLocalDescriptionWithDelegate:self
|
||||||
|
sessionDescription:sdp];
|
||||||
|
|
||||||
|
[self displayLogMessage:@"PC setLocalDescription."];
|
||||||
NSDictionary* json = @{@"type" : sdp.type, @"sdp" : sdp.description};
|
NSDictionary* json = @{@"type" : sdp.type, @"sdp" : sdp.description};
|
||||||
NSError* error;
|
NSError* error;
|
||||||
NSData* data =
|
NSData* data =
|
||||||
|
@ -442,14 +446,14 @@
|
||||||
|
|
||||||
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
- (void)peerConnection:(RTCPeerConnection*)peerConnection
|
||||||
didSetSessionDescriptionWithError:(NSError*)error {
|
didSetSessionDescriptionWithError:(NSError*)error {
|
||||||
if (error) {
|
|
||||||
[self displayLogMessage:@"SDP onFailure."];
|
|
||||||
NSAssert(NO, error.description);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self displayLogMessage:@"SDP onSuccess() - possibly drain candidates"];
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
if (error) {
|
||||||
|
[self displayLogMessage:@"SDP onFailure."];
|
||||||
|
NSAssert(NO, error.description);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self displayLogMessage:@"SDP onSuccess() - possibly drain candidates"];
|
||||||
if (!self.client.initiator) {
|
if (!self.client.initiator) {
|
||||||
if (self.peerConnection.remoteDescription &&
|
if (self.peerConnection.remoteDescription &&
|
||||||
!self.peerConnection.localDescription) {
|
!self.peerConnection.localDescription) {
|
||||||
|
|
|
@ -32,9 +32,9 @@
|
||||||
// The view controller that is displayed when AppRTCDemo is loaded.
|
// The view controller that is displayed when AppRTCDemo is loaded.
|
||||||
@interface APPRTCViewController : UIViewController<UITextFieldDelegate>
|
@interface APPRTCViewController : UIViewController<UITextFieldDelegate>
|
||||||
|
|
||||||
@property(weak, nonatomic) IBOutlet UITextField* textField;
|
@property(weak, nonatomic) IBOutlet UITextField* roomInput;
|
||||||
@property(weak, nonatomic) IBOutlet UITextView* textInstructions;
|
@property(weak, nonatomic) IBOutlet UITextView* instructionsView;
|
||||||
@property(weak, nonatomic) IBOutlet UITextView* textOutput;
|
@property(weak, nonatomic) IBOutlet UITextView* logView;
|
||||||
@property(weak, nonatomic) IBOutlet UIView* blackView;
|
@property(weak, nonatomic) IBOutlet UIView* blackView;
|
||||||
|
|
||||||
@property(nonatomic, strong) APPRTCVideoView* remoteVideoView;
|
@property(nonatomic, strong) APPRTCVideoView* remoteVideoView;
|
||||||
|
|
|
@ -41,8 +41,8 @@
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
self.statusBarOrientation =
|
self.statusBarOrientation =
|
||||||
[UIApplication sharedApplication].statusBarOrientation;
|
[UIApplication sharedApplication].statusBarOrientation;
|
||||||
self.textField.delegate = self;
|
self.roomInput.delegate = self;
|
||||||
[self.textField becomeFirstResponder];
|
[self.roomInput becomeFirstResponder];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidLayoutSubviews {
|
- (void)viewDidLayoutSubviews {
|
||||||
|
@ -59,18 +59,20 @@
|
||||||
- (void)displayText:(NSString*)text {
|
- (void)displayText:(NSString*)text {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
NSString* output =
|
NSString* output =
|
||||||
[NSString stringWithFormat:@"%@\n%@", self.textOutput.text, text];
|
[NSString stringWithFormat:@"%@\n%@", self.logView.text, text];
|
||||||
self.textOutput.text = output;
|
self.logView.text = output;
|
||||||
|
[self.logView
|
||||||
|
scrollRangeToVisible:NSMakeRange([self.logView.text length], 0)];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)resetUI {
|
- (void)resetUI {
|
||||||
[self.textField resignFirstResponder];
|
[self.roomInput resignFirstResponder];
|
||||||
self.textField.text = nil;
|
self.roomInput.text = nil;
|
||||||
self.textField.hidden = NO;
|
self.roomInput.hidden = NO;
|
||||||
self.textInstructions.hidden = NO;
|
self.instructionsView.hidden = NO;
|
||||||
self.textOutput.hidden = YES;
|
self.logView.hidden = YES;
|
||||||
self.textOutput.text = nil;
|
self.logView.text = nil;
|
||||||
self.blackView.hidden = YES;
|
self.blackView.hidden = YES;
|
||||||
|
|
||||||
[_remoteVideoView renderVideoTrackInterface:nil];
|
[_remoteVideoView renderVideoTrackInterface:nil];
|
||||||
|
@ -145,8 +147,8 @@ enum {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
textField.hidden = YES;
|
textField.hidden = YES;
|
||||||
self.textInstructions.hidden = YES;
|
self.instructionsView.hidden = YES;
|
||||||
self.textOutput.hidden = NO;
|
self.logView.hidden = NO;
|
||||||
// TODO(hughv): Instead of launching a URL with apprtc scheme, change to
|
// TODO(hughv): Instead of launching a URL with apprtc scheme, change to
|
||||||
// prepopulating the textField with a valid URL missing the room. This allows
|
// prepopulating the textField with a valid URL missing the room. This allows
|
||||||
// the user to have the simplicity of just entering the room or the ability to
|
// the user to have the simplicity of just entering the room or the ability to
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
@protocol GAEMessageHandler<NSObject>
|
@protocol GAEMessageHandler<NSObject>
|
||||||
|
|
||||||
- (void)onOpen;
|
- (void)onOpen;
|
||||||
- (void)onMessage:(NSString *)data;
|
- (void)onMessage:(NSDictionary*)data;
|
||||||
- (void)onClose;
|
- (void)onClose;
|
||||||
- (void)onError:(int)code withDescription:(NSString *)description;
|
- (void)onError:(int)code withDescription:(NSString *)description;
|
||||||
|
|
||||||
|
|
|
@ -63,41 +63,54 @@
|
||||||
|
|
||||||
#pragma mark - UIWebViewDelegate method
|
#pragma mark - UIWebViewDelegate method
|
||||||
|
|
||||||
|
+ (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;
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)webView:(UIWebView*)webView
|
- (BOOL)webView:(UIWebView*)webView
|
||||||
shouldStartLoadWithRequest:(NSURLRequest*)request
|
shouldStartLoadWithRequest:(NSURLRequest*)request
|
||||||
navigationType:(UIWebViewNavigationType)navigationType {
|
navigationType:(UIWebViewNavigationType)navigationType {
|
||||||
NSString* scheme = [request.URL scheme];
|
NSString* scheme = [request.URL scheme];
|
||||||
if ([scheme compare:@"js-frame"] != NSOrderedSame) {
|
NSAssert(scheme, @"scheme is nil: %@", request);
|
||||||
|
if (![scheme isEqualToString:@"js-frame"]) {
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
NSString* resourceSpecifier = [request.URL resourceSpecifier];
|
|
||||||
NSRange range = [resourceSpecifier rangeOfString:@":"];
|
|
||||||
NSString* method;
|
|
||||||
NSString* message;
|
|
||||||
if (range.length == 0 && range.location == NSNotFound) {
|
|
||||||
method = resourceSpecifier;
|
|
||||||
} else {
|
|
||||||
method = [resourceSpecifier substringToIndex:range.location];
|
|
||||||
message = [resourceSpecifier substringFromIndex:range.location + 1];
|
|
||||||
}
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
if ([method compare:@"onopen"] == NSOrderedSame) {
|
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];
|
[self.delegate onOpen];
|
||||||
} else if ([method compare:@"onmessage"] == NSOrderedSame) {
|
} else if ([method isEqualToString:@"onmessage"]) {
|
||||||
[self.delegate onMessage:message];
|
NSDictionary* payloadData =
|
||||||
} else if ([method compare:@"onclose"] == NSOrderedSame) {
|
[GAEChannelClient jsonStringToDictionary:payload[@"data"]];
|
||||||
|
[self.delegate onMessage:payloadData];
|
||||||
|
} else if ([method isEqualToString:@"onclose"]) {
|
||||||
[self.delegate onClose];
|
[self.delegate onClose];
|
||||||
} else if ([method compare:@"onerror"] == NSOrderedSame) {
|
} else if ([method isEqualToString:@"onerror"]) {
|
||||||
// TODO(hughv): Get error.
|
NSNumber* codeNumber = payload[@"code"];
|
||||||
int code = -1;
|
int code = [codeNumber intValue];
|
||||||
NSString* description = message;
|
NSAssert([codeNumber isEqualToNumber:[NSNumber numberWithInt:code]],
|
||||||
[self.delegate onError:code withDescription:description];
|
@"Unexpected non-integral code: %@", payload);
|
||||||
|
[self.delegate onError:code withDescription:payload[@"description"]];
|
||||||
} else {
|
} else {
|
||||||
NSAssert(
|
NSAssert(NO, @"Invalid message sent from UIWebView: %@", queuedMessage);
|
||||||
NO, @"Invalid message sent from UIWebView: %@", resourceSpecifier);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return YES;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -178,7 +178,7 @@
|
||||||
</object>
|
</object>
|
||||||
<object class="IBConnectionRecord">
|
<object class="IBConnectionRecord">
|
||||||
<object class="IBCocoaTouchOutletConnection" key="connection">
|
<object class="IBCocoaTouchOutletConnection" key="connection">
|
||||||
<string key="label">textField</string>
|
<string key="label">roomInput</string>
|
||||||
<reference key="source" ref="372490531"/>
|
<reference key="source" ref="372490531"/>
|
||||||
<reference key="destination" ref="546385578"/>
|
<reference key="destination" ref="546385578"/>
|
||||||
</object>
|
</object>
|
||||||
|
@ -186,7 +186,7 @@
|
||||||
</object>
|
</object>
|
||||||
<object class="IBConnectionRecord">
|
<object class="IBConnectionRecord">
|
||||||
<object class="IBCocoaTouchOutletConnection" key="connection">
|
<object class="IBCocoaTouchOutletConnection" key="connection">
|
||||||
<string key="label">textInstructions</string>
|
<string key="label">instructionsView</string>
|
||||||
<reference key="source" ref="372490531"/>
|
<reference key="source" ref="372490531"/>
|
||||||
<reference key="destination" ref="176994284"/>
|
<reference key="destination" ref="176994284"/>
|
||||||
</object>
|
</object>
|
||||||
|
@ -194,7 +194,7 @@
|
||||||
</object>
|
</object>
|
||||||
<object class="IBConnectionRecord">
|
<object class="IBConnectionRecord">
|
||||||
<object class="IBCocoaTouchOutletConnection" key="connection">
|
<object class="IBCocoaTouchOutletConnection" key="connection">
|
||||||
<string key="label">textOutput</string>
|
<string key="label">logView</string>
|
||||||
<reference key="source" ref="372490531"/>
|
<reference key="source" ref="372490531"/>
|
||||||
<reference key="destination" ref="634862110"/>
|
<reference key="destination" ref="634862110"/>
|
||||||
</object>
|
</object>
|
||||||
|
@ -660,25 +660,25 @@
|
||||||
<string key="superclassName">UIViewController</string>
|
<string key="superclassName">UIViewController</string>
|
||||||
<dictionary class="NSMutableDictionary" key="outlets">
|
<dictionary class="NSMutableDictionary" key="outlets">
|
||||||
<string key="blackView">UIView</string>
|
<string key="blackView">UIView</string>
|
||||||
<string key="textField">UITextField</string>
|
<string key="roomInput">UITextField</string>
|
||||||
<string key="textInstructions">UITextView</string>
|
<string key="instructionsView">UITextView</string>
|
||||||
<string key="textOutput">UITextView</string>
|
<string key="logView">UITextView</string>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
<dictionary class="NSMutableDictionary" key="toOneOutletInfosByName">
|
<dictionary class="NSMutableDictionary" key="toOneOutletInfosByName">
|
||||||
<object class="IBToOneOutletInfo" key="blackView">
|
<object class="IBToOneOutletInfo" key="blackView">
|
||||||
<string key="name">blackView</string>
|
<string key="name">blackView</string>
|
||||||
<string key="candidateClassName">UIView</string>
|
<string key="candidateClassName">UIView</string>
|
||||||
</object>
|
</object>
|
||||||
<object class="IBToOneOutletInfo" key="textField">
|
<object class="IBToOneOutletInfo" key="roomInput">
|
||||||
<string key="name">textField</string>
|
<string key="name">roomInput</string>
|
||||||
<string key="candidateClassName">UITextField</string>
|
<string key="candidateClassName">UITextField</string>
|
||||||
</object>
|
</object>
|
||||||
<object class="IBToOneOutletInfo" key="textInstructions">
|
<object class="IBToOneOutletInfo" key="instructionsView">
|
||||||
<string key="name">textInstructions</string>
|
<string key="name">instructionsView</string>
|
||||||
<string key="candidateClassName">UITextView</string>
|
<string key="candidateClassName">UITextView</string>
|
||||||
</object>
|
</object>
|
||||||
<object class="IBToOneOutletInfo" key="textOutput">
|
<object class="IBToOneOutletInfo" key="logView">
|
||||||
<string key="name">textOutput</string>
|
<string key="name">logView</string>
|
||||||
<string key="candidateClassName">UITextView</string>
|
<string key="candidateClassName">UITextView</string>
|
||||||
</object>
|
</object>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
Helper HTML that redirects Google AppEngine's Channel API to Objective C.
|
Helper HTML that redirects Google AppEngine's Channel API to Objective C.
|
||||||
This is done by hosting this page in an iOS application. The hosting
|
This is done by hosting this page in an iOS application. The hosting
|
||||||
class creates a UIWebView control and implements the UIWebViewDelegate
|
class creates a UIWebView control and implements the UIWebViewDelegate
|
||||||
protocol. Then when there is a channel message, it is encoded in an IFRAME.
|
protocol. Then when there is a channel message it is queued in JS,
|
||||||
That IFRAME is added to the DOM which triggers a navigation event
|
and an IFRAME is added to the DOM, triggering a navigation event
|
||||||
|shouldStartLoadWithRequest| in Objective C which can then be routed in the
|
|shouldStartLoadWithRequest| in Objective C which can then fetch the
|
||||||
application as desired.
|
message using |popQueuedMessage|. This queuing is necessary to avoid URL
|
||||||
|
length limits in UIWebView (which are undocumented).
|
||||||
-->
|
-->
|
||||||
<body onbeforeunload="closeSocket()" onload="openSocket()">
|
<body onbeforeunload="closeSocket()" onload="openSocket()">
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
@ -38,6 +39,10 @@
|
||||||
|
|
||||||
var channel = null;
|
var channel = null;
|
||||||
var socket = null;
|
var socket = null;
|
||||||
|
// In-order queue of messages to be delivered to ObjectiveC.
|
||||||
|
// Each is a JSON.stringify()'d dictionary containing a 'type'
|
||||||
|
// field and optionally a 'payload'.
|
||||||
|
var messageQueue = [];
|
||||||
|
|
||||||
function openSocket() {
|
function openSocket() {
|
||||||
if (!QueryString.token || !QueryString.token.match(/^[A-z0-9_-]+$/)) {
|
if (!QueryString.token || !QueryString.token.match(/^[A-z0-9_-]+$/)) {
|
||||||
|
@ -52,17 +57,13 @@
|
||||||
sendMessageToObjC("onopen");
|
sendMessageToObjC("onopen");
|
||||||
},
|
},
|
||||||
'onmessage': function(msg) {
|
'onmessage': function(msg) {
|
||||||
sendMessageToObjC("onmessage:" +
|
sendMessageToObjC("onmessage", msg);
|
||||||
encodeURIComponent(JSON.stringify(msg.data)));
|
|
||||||
},
|
},
|
||||||
'onclose': function() {
|
'onclose': function() {
|
||||||
sendMessageToObjC("onclose");
|
sendMessageToObjC("onclose");
|
||||||
},
|
},
|
||||||
'onerror': function(err) {
|
'onerror': function(err) {
|
||||||
sendMessageToObjC("onerror:" +
|
sendMessageToObjC("onerror", err);
|
||||||
encodeURIComponent(JSON.stringify(err.code)) +
|
|
||||||
":message:" +
|
|
||||||
encodeURIComponent(JSON.stringify(err.description)));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -73,9 +74,10 @@
|
||||||
|
|
||||||
// Add an IFRAME to the DOM to trigger a navigation event. Then remove
|
// Add an IFRAME to the DOM to trigger a navigation event. Then remove
|
||||||
// it as it is no longer needed. Only one event is generated.
|
// it as it is no longer needed. Only one event is generated.
|
||||||
function sendMessageToObjC(message) {
|
function sendMessageToObjC(type, payload) {
|
||||||
|
messageQueue.push(JSON.stringify({'type': type, 'payload': payload}));
|
||||||
var iframe = document.createElement("IFRAME");
|
var iframe = document.createElement("IFRAME");
|
||||||
iframe.setAttribute("src", "js-frame:" + message);
|
iframe.setAttribute("src", "js-frame:");
|
||||||
// For some reason we need to set a non-empty size for the iOS6
|
// For some reason we need to set a non-empty size for the iOS6
|
||||||
// simulator...
|
// simulator...
|
||||||
iframe.setAttribute("height", "1px");
|
iframe.setAttribute("height", "1px");
|
||||||
|
@ -83,6 +85,10 @@
|
||||||
document.documentElement.appendChild(iframe);
|
document.documentElement.appendChild(iframe);
|
||||||
iframe.parentNode.removeChild(iframe);
|
iframe.parentNode.removeChild(iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function popQueuedMessage() {
|
||||||
|
return messageQueue.shift();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue