diff --git a/Classes/NSData-BytePacking.h b/Classes/NSData-BytePacking.h new file mode 100644 index 0000000..2b78429 --- /dev/null +++ b/Classes/NSData-BytePacking.h @@ -0,0 +1,61 @@ +// +// NSData-BytePacking.h +// +// Created by Philippe Hausler on 9/10/08. +// Copyright 2008 Philippe Hausler. All rights reserved. +// + +#import + +enum { + NSLittleEndian = -1, + NSSameEndian = 0, + NSBigEndian = 1 +}; +typedef NSInteger NSByteEndian; + +NSByteEndian NSHostEndian(void); + +@interface NSString (BytePacking) ++ (id)stringWithData:(NSData *)data encoding:(NSStringEncoding)encoding; +@end + +@interface NSData (BytePacking) +- (NSString *)stringUsingEncoding:(NSStringEncoding)encoding; +- (BOOL)hasPrefix:(NSData *)data; +- (BOOL)hasSuffix:(NSData *)data; +- (BOOL)hasUTF8Prefix:(NSString *)prefix; +- (BOOL)hasUTF8Suffix:(NSString *)suffix; ++ (NSData *)dataWithUTF8String:(NSString *)aString; +- (unsigned char)byteAtIndex:(NSUInteger)index; +- (unsigned int)uintAtIndex:(NSUInteger)index; +- (NSData *)dataUntilTermination:(NSData *)terminator; ++ (id)dataWithByte:(unsigned char)byte; +@end + +@interface NSMutableData (BytePacking) +- (void)pad:(NSData *)pad count:(NSUInteger)count; + +- (void)appendString:(NSString *)aString encoding:(NSStringEncoding)encoding; + +- (void)appendByte:(unsigned char)byte; +- (void)appendShort:(short)number; +- (void)appendUShort:(unsigned short)number; +- (void)appendInt:(int)number; +- (void)appendUInt:(unsigned int)number; +- (void)appendLong:(long)number; +- (void)appendULong:(unsigned long)number; +- (void)appendDouble:(double)number; +- (void)appendLongLong:(long long)number; +- (void)appendULongLong:(unsigned long long)number; + +- (void)appendShort:(short)number ordered:(NSByteEndian)endian; +- (void)appendUShort:(unsigned short)number ordered:(NSByteEndian)endian; +- (void)appendInt:(int)number ordered:(NSByteEndian)endian; +- (void)appendUInt:(unsigned int)number ordered:(NSByteEndian)endian; +- (void)appendLong:(long)number ordered:(NSByteEndian)endian; +- (void)appendULong:(unsigned long)number ordered:(NSByteEndian)endian; +- (void)appendDouble:(double)number ordered:(NSByteEndian)endian; +- (void)appendLongLong:(long long)number ordered:(NSByteEndian)endian; +- (void)appendULongLong:(unsigned long long)number ordered:(NSByteEndian)endian; +@end diff --git a/Classes/NSData-BytePacking.m b/Classes/NSData-BytePacking.m new file mode 100644 index 0000000..df00f84 --- /dev/null +++ b/Classes/NSData-BytePacking.m @@ -0,0 +1,317 @@ +// +// NSData-BytePacking.m +// +// Created by Philippe Hausler on 9/10/08. +// Copyright 2008 Philippe Hausler. All rights reserved. +// + +#import "NSData-BytePacking.h" +NSByteEndian NSHostEndian() +{ + if(NSHostByteOrder() == NS_LittleEndian) + { + return NSLittleEndian; + } + else if(NSHostByteOrder() == NS_BigEndian) + { + return NSBigEndian; + } + return NS_UnknownByteOrder; +} + +@implementation NSString (BytePacking) ++ (id)stringWithData:(NSData *)data encoding:(NSStringEncoding)encoding +{ + return [[[NSString alloc] initWithData:data encoding:encoding] autorelease]; +} +@end + +@implementation NSData (BytePacking) +- (NSString *)stringUsingEncoding:(NSStringEncoding)encoding +{ + return [NSString stringWithData:self encoding:encoding]; +} +- (BOOL)hasPrefix:(NSData *)data +{ + return [[self subdataWithRange:NSMakeRange(0, [data length])] isEqualToData:data]; +} +- (BOOL)hasSuffix:(NSData *)data +{ + return [[self subdataWithRange:NSMakeRange([self length]-[data length]-1, [data length])] isEqualToData:data]; +} +- (BOOL)hasUTF8Prefix:(NSString *)prefix +{ + return [self hasPrefix:[prefix dataUsingEncoding:NSUTF8StringEncoding]]; +} +- (BOOL)hasUTF8Suffix:(NSString *)suffix +{ + return [self hasSuffix:[suffix dataUsingEncoding:NSUTF8StringEncoding]]; +} ++ (NSData *)dataWithUTF8String:(NSString *)aString +{ + return [aString dataUsingEncoding:NSUTF8StringEncoding]; +} +- (unsigned char)byteAtIndex:(NSUInteger)index +{ + unsigned char byte = 0x00; + [self getBytes:&byte range:NSMakeRange(index, 1)]; + return byte; +} +- (unsigned int)uintAtIndex:(NSUInteger)index +{ + unsigned int number = 0x00; + [self getBytes:&number range:NSMakeRange(index, sizeof(unsigned int))]; + return number; +} +- (NSData *)dataUntilTermination:(NSData *)terminator +{ + NSUInteger pos = 0; + while(pos < [self length]) + { + if([[self subdataWithRange:NSMakeRange(pos, [terminator length])] isEqualToData:terminator]) + { + return [self subdataWithRange:NSMakeRange(0, pos)]; + } + pos++; + } + return NULL; +} ++ (id)dataWithByte:(unsigned char)byte +{ + return [NSData dataWithBytes:&byte length:1]; +} +@end + +@implementation NSMutableData (BytePacking) +- (void)pad:(NSData *)pad count:(NSUInteger)count +{ + for(NSUInteger i = 0; i < count; i++) + { + [self appendData:pad]; + } +} +- (void)appendString:(NSString *)aString encoding:(NSStringEncoding)encoding +{ + [self appendData:[aString dataUsingEncoding:encoding]]; +} +- (void)appendByte:(unsigned char)byte +{ + [self appendBytes:&byte length:1]; +} +- (void)appendShort:(short)number +{ + [self appendShort:number ordered:NSSameEndian]; +} +- (void)appendUShort:(unsigned short)number +{ + [self appendUShort:number ordered:NSSameEndian]; +} +- (void)appendInt:(int)number +{ + [self appendInt:number ordered:NSSameEndian]; +} +- (void)appendUInt:(unsigned int)number +{ + [self appendUInt:number ordered:NSSameEndian]; +} +- (void)appendLong:(long)number +{ + [self appendLong:number ordered:NSSameEndian]; +} +- (void)appendULong:(unsigned long)number +{ + [self appendULong:number ordered:NSSameEndian]; +} +- (void)appendDouble:(double)number +{ + [self appendULong:number ordered:NSSameEndian]; +} +- (void)appendLongLong:(long long)number +{ + [self appendLongLong:number ordered:NSSameEndian]; +} +- (void)appendULongLong:(unsigned long long)number +{ + [self appendLongLong:number ordered:NSSameEndian]; +} + +- (void)appendShort:(short)number ordered:(NSByteEndian)endian +{ + switch (endian) { + case NSLittleEndian: { + short bytes = NSSwapHostShortToLittle((unsigned short)number); + [self appendBytes:&bytes length:sizeof(short)]; + break; + } + case NSBigEndian: { + short bytes = NSSwapHostShortToBig((unsigned short)number); + [self appendBytes:&bytes length:sizeof(short)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(short)]; + break; + } + } +} +- (void)appendUShort:(unsigned short)number ordered:(NSByteEndian)endian +{ + switch (endian) { + case NSLittleEndian: { + unsigned short bytes = NSSwapHostShortToLittle(number); + [self appendBytes:&bytes length:sizeof(unsigned short)]; + break; + } + case NSBigEndian: { + unsigned short bytes = NSSwapHostShortToBig(number); + [self appendBytes:&bytes length:sizeof(unsigned short)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(unsigned short)]; + break; + } + } +} + +- (void)appendInt:(int)number ordered:(NSByteEndian)endian +{ + switch(endian) + { + case NSLittleEndian: { + int bytes = NSSwapHostIntToLittle((unsigned int)number); + [self appendBytes:&bytes length:sizeof(int)]; + break; + } + case NSBigEndian: { + int bytes = NSSwapHostIntToBig((unsigned int)number); + [self appendBytes:&bytes length:sizeof(int)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(int)]; + break; + } + } +} +- (void)appendUInt:(unsigned int)number ordered:(NSByteEndian)endian +{ + switch(endian) + { + case NSLittleEndian: { + unsigned int bytes = NSSwapHostIntToLittle(number); + [self appendBytes:&bytes length:sizeof(unsigned int)]; + break; + } + case NSBigEndian: { + unsigned int bytes = NSSwapHostIntToBig(number); + [self appendBytes:&bytes length:sizeof(unsigned int)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(unsigned int)]; + break; + } + } +} +- (void)appendLong:(long)number ordered:(NSByteEndian)endian +{ + switch(endian) + { + case NSLittleEndian: { + long bytes = NSSwapHostLongToLittle((unsigned long)number); + [self appendBytes:&bytes length:sizeof(long)]; + break; + } + case NSBigEndian: { + long bytes = NSSwapHostLongToBig((unsigned long)number); + [self appendBytes:&bytes length:sizeof(long)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(long)]; + break; + } + } +} +- (void)appendULong:(unsigned long)number ordered:(NSByteEndian)endian +{ + switch(endian) + { + case NSLittleEndian: { + unsigned long bytes = NSSwapHostLongToLittle(number); + [self appendBytes:&bytes length:sizeof(unsigned long)]; + break; + } + case NSBigEndian: { + unsigned long bytes = NSSwapHostLongToBig(number); + [self appendBytes:&bytes length:sizeof(unsigned long)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(unsigned long)]; + break; + } + } +} +- (void)appendDouble:(double)number ordered:(NSByteEndian)endian +{ + switch(endian) + { + case NSLittleEndian: { + NSSwappedDouble bytes = NSSwapHostDoubleToLittle(number); + [self appendBytes:&bytes length:sizeof(NSSwappedDouble)]; + break; + } + case NSBigEndian: { + NSSwappedDouble bytes = NSSwapHostDoubleToBig(number); + [self appendBytes:&bytes length:sizeof(NSSwappedDouble)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(double)]; + break; + } + } +} +- (void)appendLongLong:(long long)number ordered:(NSByteEndian)endian +{ + switch(endian) + { + case NSLittleEndian: { + long long bytes = NSSwapHostLongLongToLittle((unsigned long long)number); + [self appendBytes:&bytes length:sizeof(long long)]; + break; + } + case NSBigEndian: { + long long bytes = NSSwapHostLongToBig((unsigned long long)number); + [self appendBytes:&bytes length:sizeof(long long)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(long long)]; + break; + } + } +} +- (void)appendULongLong:(unsigned long long)number ordered:(NSByteEndian)endian +{ + switch(endian) + { + case NSLittleEndian: { + unsigned long long bytes = NSSwapHostLongToLittle(number); + [self appendBytes:&bytes length:sizeof(unsigned long long)]; + break; + } + case NSBigEndian: { + unsigned long long bytes = NSSwapHostLongToBig(number); + [self appendBytes:&bytes length:sizeof(unsigned long long)]; + break; + } + case NSSameEndian: { + [self appendBytes:&number length:sizeof(unsigned long long)]; + break; + } + } +} +@end diff --git a/Classes/RTMPStream.h b/Classes/RTMPStream.h new file mode 100644 index 0000000..0f40f74 --- /dev/null +++ b/Classes/RTMPStream.h @@ -0,0 +1,19 @@ +// +// RTMPStream.h +// Tuve +// +// Created by Philippe Hausler on 9/10/08. +// Copyright 2008 Philippe Hausler. All rights reserved. +// + +#import +#import "TCPSocket.h" + +@interface RTMPStream : NSObject { + TCPSocket *socket; + NSFileHandle *connection; + NSMutableData *buffer; +} +- (void)connectToServer:(NSString *)server onPort:(unsigned short)port; +- (void)requestStream:(NSString *)file; +@end diff --git a/Classes/RTMPStream.m b/Classes/RTMPStream.m new file mode 100644 index 0000000..a2aec5c --- /dev/null +++ b/Classes/RTMPStream.m @@ -0,0 +1,193 @@ +// +// 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" + +/*Constants sourced from VLC's rtmp_amf_flv.c*/ +const unsigned char RTMP_HANDSHAKE = 0x03; +const unsigned short RTMP_HANDSHAKE_BODY_SIZE = 1536; + +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)sendHandshake; +- (void)disconnect; +- (void)sendConnectRequest; +- (void)waitForHandshakeResponse; + +- (NSData *)encodeAMFString:(NSString *)str; +- (NSData *)encodeAMFNumber:(unsigned long long)number; +- (NSData *)encodeAMFBoolean:(BOOL)value; +- (NSData *)encodeAMFObject:(NSData *)object; +- (NSData *)encodeAMFStringVariable:(NSString *)value key:(NSString *)key; +- (NSData *)encodeAMFBooleanVariable:(BOOL)value key:(NSString *)key; +@end +@implementation RTMPStream +- (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)requestStream:(NSString *)file +{ + NSMutableData *packet = [NSMutableData data]; + +} +- (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]; + [data appendData:[self encodeAMFString:@"connect"]]; + [data appendData:[self encodeAMFNumber:AMF_CALL_NETCONNECTION_CONNECT]]; + [data appendData:[self encodeAMFObject:NULL]]; + [data appendData:[self encodeAMFStringVariable:@"TUvé" key:@"app"]]; + [data appendData:[self encodeAMFStringVariable:@"LNX 9,0,48,0" key:@"flashVer"]]; + [data appendData:[self encodeAMFStringVariable:@"file://iphone.flv" key:@"swfUrl"]]; +} +- (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 *)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 *)encodeAMFBooleanVariable:(BOOL)value key:(NSString *)key +{ + NSMutableData *encodedVariable = [NSMutableData data]; + [encodedVariable appendUShort:[key length] ordered:NSBigEndian]; + [encodedVariable appendString:key encoding:NSUTF8StringEncoding]; + [encodedVariable appendData:[self encodedAMFBoolean:value]]; + return encodedVariable; +} +@end diff --git a/Classes/TuveAppDelegate.h b/Classes/TuveAppDelegate.h index 2fab28c..a5ca63f 100644 --- a/Classes/TuveAppDelegate.h +++ b/Classes/TuveAppDelegate.h @@ -17,6 +17,7 @@ IBOutlet UITextField *chatBox; TuveConnection *connection; BOOL flipped; + MPMoviePlayerController *player; } @property (nonatomic, retain) UIWindow *window; diff --git a/Classes/TuveAppDelegate.m b/Classes/TuveAppDelegate.m index 67169ea..5ffdd65 100644 --- a/Classes/TuveAppDelegate.m +++ b/Classes/TuveAppDelegate.m @@ -5,7 +5,7 @@ // Created by Philippe Hausler on 9/8/08. // Copyright Philippe Hausler 2008. All rights reserved. // - +#import #import "TuveAppDelegate.h" #import "TuveConnection.h" @implementation TuveAppDelegate @@ -14,12 +14,9 @@ - (void)applicationDidFinishLaunching:(UIApplication *)application { connection = [[TuveConnection alloc] init]; - //NSURL *test = [NSURL URLWithString:@"rtmp://rtmp.ubixonline.com/Titiyo%20-%20Come%20Along.flv"]; - //NSLog(@"%@", test); flipped = NO; [window makeKeyAndVisible]; [window addSubview:nav.view]; - } - (IBAction)toggleView { @@ -83,4 +80,33 @@ [UIView commitAnimations]; return YES; } +-(void)playMovieAtURL:(NSURL*)theURL + +{ + MPMoviePlayerController* theMovie=[[MPMoviePlayerController alloc] initWithContentURL:theURL]; + theMovie.scalingMode=MPMovieScalingModeAspectFill; + theMovie.movieControlMode=MPMovieControlModeHidden; + + // Register for the playback finished notification. + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(movieFinished:) + name:MPMoviePlayerPlaybackDidFinishNotification + object:theMovie]; + + // Movie playback is asynchronous, so this method returns immediately. + [theMovie play]; +} + +// When the movie is done,release the controller. +-(void)movieFinished:(NSNotification*)aNotification +{ + MPMoviePlayerController* theMovie=[aNotification object]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:MPMoviePlayerPlaybackDidFinishNotification + object:theMovie]; + + // Release the movie instance created in playMovieAtURL + [theMovie release]; +} @end