// // RTMPStream.m // Tuve // // Created by Philippe Hausler on 9/10/08. // Copyright 2008 Philippe Hausler. All rights reserved. // #import "RTMPStream.h" #import "NSData-BytePacking.h" const unsigned short RTMP_DEFAULT_PORT = 1935; /*Constants sourced from VLC's rtmp_amf_flv.c*/ const unsigned char RTMP_HANDSHAKE = 0x03; const unsigned short RTMP_HANDSHAKE_BODY_SIZE = 1536; /* content types */ const unsigned char RTMP_CONTENT_TYPE_CHUNK_SIZE = 0x01; const unsigned char RTMP_CONTENT_TYPE_UNKNOWN_02 = 0x02; const unsigned char RTMP_CONTENT_TYPE_BYTES_READ = 0x03; const unsigned char RTMP_CONTENT_TYPE_PING = 0x04; const unsigned char RTMP_CONTENT_TYPE_SERVER_BW = 0x05; const unsigned char RTMP_CONTENT_TYPE_CLIENT_BW = 0x06; const unsigned char RTMP_CONTENT_TYPE_UNKNOWN_07 = 0x07; const unsigned char RTMP_CONTENT_TYPE_AUDIO_DATA = 0x08; const unsigned char RTMP_CONTENT_TYPE_VIDEO_DATA = 0x09; const unsigned char RTMP_CONTENT_TYPE_UNKNOWN_0A_0E = 0x0A; const unsigned char RTMP_CONTENT_TYPE_FLEX_STREAM = 0x0F; const unsigned char RTMP_CONTENT_TYPE_FLEX_SHARED_OBJECT = 0x10; const unsigned char RTMP_CONTENT_TYPE_MESSAGE = 0x11; const unsigned char RTMP_CONTENT_TYPE_NOTIFY = 0x12; const unsigned char RTMP_CONTENT_TYPE_SHARED_OBJECT = 0x13; const unsigned char RTMP_CONTENT_TYPE_INVOKE = 0x14; const unsigned char AMF_BOOLEAN_FALSE = 0x00; const unsigned char AMF_BOOLEAN_TRUE = 0x01; /* datatypes */ const unsigned char AMF_DATATYPE_NUMBER = 0x00; const unsigned char AMF_DATATYPE_BOOLEAN = 0x01; const unsigned char AMF_DATATYPE_STRING = 0x02; const unsigned char AMF_DATATYPE_OBJECT = 0x03; const unsigned char AMF_DATATYPE_MOVIE_CLIP = 0x04; const unsigned char AMF_DATATYPE_NULL = 0x05; const unsigned char AMF_DATATYPE_UNDEFINED = 0x06; const unsigned char AMF_DATATYPE_REFERENCE = 0x07; const unsigned char AMF_DATATYPE_MIXED_ARRAY = 0x08; const unsigned char AMF_DATATYPE_END_OF_OBJECT = 0x09; const unsigned char AMF_DATATYPE_ARRAY = 0x0A; const unsigned char AMF_DATATYPE_DATE = 0x0B; const unsigned char AMF_DATATYPE_LONG_STRING = 0x0C; const unsigned char AMF_DATATYPE_UNSUPPORTED = 0x0D; const unsigned char AMF_DATATYPE_RECORDSET = 0x0E; const unsigned char AMF_DATATYPE_XML = 0x0F; const unsigned char AMF_DATATYPE_TYPED_OBJECT = 0x10; const unsigned char AMF_DATATYPE_AMF3_DATA = 0x11; /* datatypes sizes */ const unsigned char AMF_DATATYPE_SIZE_NUMBER = 9; const unsigned char AMF_DATATYPE_SIZE_BOOLEAN = 2; const unsigned char AMF_DATATYPE_SIZE_STRING = 3; const unsigned char AMF_DATATYPE_SIZE_OBJECT = 1; const unsigned char AMF_DATATYPE_SIZE_NULL = 1; const unsigned char AMF_DATATYPE_SIZE_OBJECT_VARIABLE = 2; const unsigned char AMF_DATATYPE_SIZE_MIXED_ARRAY = 5; const unsigned char AMF_DATATYPE_SIZE_END_OF_OBJECT = 3; /* amf remote calls */ const unsigned long long AMF_CALL_NETCONNECTION_CONNECT = 0x3FF0000000000000; const unsigned long long AMF_CALL_NETCONNECTION_CONNECT_AUDIOCODECS = 0x4083380000000000; const unsigned long long AMF_CALL_NETCONNECTION_CONNECT_VIDEOCODECS = 0x405F000000000000; const unsigned long long AMF_CALL_NETCONNECTION_CONNECT_VIDEOFUNCTION = 0x3FF0000000000000; const unsigned long long AMF_CALL_NETCONNECTION_CONNECT_OBJECTENCODING = 0x0; const double AMF_CALL_STREAM_CLIENT_NUMBER = 3.0; const double AMF_CALL_ONBWDONE = 2.0; const unsigned long long AMF_CALL_NETSTREAM_PLAY = 0x0; /*Private calls not visible to the fellow classes*/ @interface RTMPStream (Private) - (void)connectToServer:(NSString *)server onPort:(unsigned short)port; - (void)sendHandshake; - (void)disconnect; - (void)sendConnectRequest; - (void)waitForHandshakeResponse; - (void)sendPacket:(NSData *)packet type:(unsigned char)type; - (NSData *)encodePacket:(NSData *)packet type:(unsigned char)type; - (NSData *)encodeAMFString:(NSString *)str; - (NSData *)encodeAMFNumber:(unsigned long long)number; - (NSData *)encodeAMFBoolean:(BOOL)value; - (NSData *)encodeAMFObject:(NSData *)object; - (NSData *)encodeAMFEndObject; - (NSData *)encodeAMFStringVariable:(NSString *)value key:(NSString *)key; - (NSData *)encodeAMFNumberVariable:(unsigned long long)value key:(NSString *)key; - (NSData *)encodeAMFBooleanVariable:(BOOL)value key:(NSString *)key; @end @implementation RTMPStream - (NSURL *)streamURL { return streamURL; } - (void)setStreamURL:(NSURL *)aStreamURL { streamURL = [aStreamURL copy]; unsigned short port = RTMP_DEFAULT_PORT; if([streamURL port]) { port = [[streamURL port] unsignedShortValue]; } [self connectToServer:[streamURL host] onPort:port]; } - (void)connectToServer:(NSString *)server onPort:(unsigned short)port { //if for some reason we are re-connecting to the server, the socket, buffer, and file handle need to be invalidated and re-created [connection release]; [socket release]; [buffer release]; buffer = [[NSMutableData data] retain]; socket = [[TCPSocket alloc] initWithConnectionTo:server onTCPPort:port]; connection = [[NSFileHandle alloc] initWithFileDescriptor:[socket socket]]; [self sendHandshake]; } - (void)dataAvailable:(NSNotification *)aNotification { [connection waitForDataInBackgroundAndNotify]; } - (void)sendHandshake { NSMutableData *packet = [NSMutableData data]; [packet appendByte:RTMP_HANDSHAKE]; for(int i = 0; i < RTMP_HANDSHAKE_BODY_SIZE; i++) { [packet appendByte:i & 0xFF]; } [connection writeData:packet]; [self waitForHandshakeResponse]; } - (void)waitForHandshakeResponse { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handshakeResponse:) name:NSFileHandleDataAvailableNotification object:NULL]; [connection waitForDataInBackgroundAndNotify]; } - (void)handshakeResponse:(NSNotification *)aNotification { [buffer appendData:[connection availableData]]; if([buffer length] == RTMP_HANDSHAKE_BODY_SIZE * 2 + 1) //if the server has sent a response of the appropriate size we need to step forward into the protocol { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleDataAvailableNotification object:NULL]; //the notification of the handshakeResponse is no longer valid, remove it from the center if(![buffer hasPrefix:[NSData dataWithByte:RTMP_HANDSHAKE]]) { NSLog(@"Massive error: Protocol breach from the server side. Packet header RTMP_HANDSHAKE expected but not received. Aborting connection"); [self disconnect]; } else { //Acknowege the handshake with the first 1935 bytes after the header from the server response pair [connection writeData:[buffer subdataWithRange:NSMakeRange(1, RTMP_HANDSHAKE_BODY_SIZE)]]; [self sendConnectRequest]; } } else { [connection waitForDataInBackgroundAndNotify]; } } - (void)sendConnectRequest { NSMutableData *packet = [NSMutableData data]; [packet appendData:[self encodeAMFString:@"connect"]]; [packet appendData:[self encodeAMFNumber:AMF_CALL_NETCONNECTION_CONNECT]]; [packet appendData:[self encodeAMFObject:NULL]]; [packet appendData:[self encodeAMFStringVariable:@"TUvé" key:@"app"]]; [packet appendData:[self encodeAMFStringVariable:@"LNX 9,0,48,0" key:@"flashVer"]]; [packet appendData:[self encodeAMFStringVariable:@"file://iphone.flv" key:@"swfUrl"]]; [packet appendData:[self encodeAMFStringVariable:[streamURL absoluteString] key:@"tcUrl"]]; [packet appendData:[self encodeAMFBooleanVariable:FALSE key:@"fpad"]]; [packet appendData:[self encodeAMFNumberVariable:AMF_CALL_NETCONNECTION_CONNECT_AUDIOCODECS key:@"audioCodecs"]]; [packet appendData:[self encodeAMFNumberVariable:AMF_CALL_NETCONNECTION_CONNECT_VIDEOCODECS key:@"videoCodecs"]]; [packet appendData:[self encodeAMFNumberVariable:AMF_CALL_NETCONNECTION_CONNECT_VIDEOFUNCTION key:@"videoFunction"]]; [packet appendData:[self encodeAMFStringVariable:@"file://iphone.html" key:@"pageUrl"]]; [packet appendData:[self encodeAMFNumberVariable:AMF_CALL_NETCONNECTION_CONNECT_OBJECTENCODING key:@"objectEncoding"]]; [packet appendData:[self encodeAMFEndObject]]; [self sendPacket:packet type:RTMP_CONTENT_TYPE_INVOKE]; } - (void)sendPacket:(NSData *)packet type:(unsigned char)type { [connection writeData:[self encodePacket:connection type:type]]; } - (NSData *)encodePacket:(NSData *)packet type:(unsigned char)type { } - (NSData *)encodeAMFString:(NSString *)str { NSMutableData *encodedString = [NSMutableData dataWithByte:AMF_DATATYPE_STRING]; unsigned short len = [str length]; [encodedString appendUShort:len ordered:NSBigEndian]; [encodedString appendData:[str dataUsingEncoding:NSUTF8StringEncoding]]; return encodedString; } - (NSData *)encodeAMFNumber:(unsigned long long)number { NSMutableData *encodedNumber = [NSMutableData dataWithByte:AMF_DATATYPE_NUMBER]; [encodedNumber appendULongLong:number]; return encodedNumber; } - (NSData *)encodeAMFBoolean:(BOOL)value { NSMutableData *encodedBoolean = [NSMutableData dataWithByte:AMF_DATATYPE_BOOLEAN]; if(value) { [encodedBoolean appendByte:AMF_BOOLEAN_TRUE]; } else { [encodedBoolean appendByte:AMF_BOOLEAN_FALSE]; } return encodedBoolean; } - (NSData *)encodeAMFObject:(NSData *)object { NSMutableData *encodedObject = [NSMutableData dataWithByte:AMF_DATATYPE_OBJECT]; [encodedObject pad:[NSData dataWithByte:0x00] count:AMF_DATATYPE_SIZE_OBJECT-1]; return encodedObject; } - (NSData *)encodeAMFEndObject { NSMutableData *encodedObject = [NSMutableData data]; [encodedObject pad:[NSData dataWithByte:0x00] count:AMF_DATATYPE_SIZE_END_OF_OBJECT - 1]; [encodedObject appendByte:AMF_DATATYPE_END_OF_OBJECT]; return encodedObject; } - (NSData *)encodeAMFStringVariable:(NSString *)value key:(NSString *)key { NSMutableData *encodedVariable = [NSMutableData data]; [encodedVariable appendUShort:[key length] ordered:NSBigEndian]; [encodedVariable appendString:key encoding:NSUTF8StringEncoding]; [encodedVariable appendData:[self encodeAMFString:value]]; return encodedVariable; } - (NSData *)encodeAMFNumberVariable:(unsigned long long)value key:(NSString *)key { NSMutableData *encodedVariable = [NSMutableData data]; [encodedVariable appendUShort:[key length] ordered:NSBigEndian]; [encodedVariable appendString:key encoding:NSUTF8StringEncoding]; [encodedVariable appendData:[self encodeAMFNumber:value]]; return encodedVariable; } - (NSData *)encodeAMFBooleanVariable:(BOOL)value key:(NSString *)key { NSMutableData *encodedVariable = [NSMutableData data]; [encodedVariable appendUShort:[key length] ordered:NSBigEndian]; [encodedVariable appendString:key encoding:NSUTF8StringEncoding]; [encodedVariable appendData:[self encodeAMFBoolean:value]]; return encodedVariable; } @end