diff options
author | Sho Amano <samano@xevo.com> | 2017-04-14 20:21:55 +0900 |
---|---|---|
committer | Sho Amano <samano@xevo.com> | 2017-08-23 15:11:36 +0900 |
commit | 0e5ec4087e6b1624a7815caf95acabf5c68b68ae (patch) | |
tree | 063211a626624781991f23eda18c8e74cdf67da6 | |
parent | 07f9f71a56d79bab7493d3488c10da51ebd407aa (diff) | |
download | sdl_ios-0e5ec4087e6b1624a7815caf95acabf5c68b68ae.tar.gz |
Add RTP packetizer
This packetizer sends H.264 video stream using
RTP frames defined by RFC 6184 and RFC 4571.
-rw-r--r-- | SmartDeviceLink-iOS.xcodeproj/project.pbxproj | 12 | ||||
-rw-r--r-- | SmartDeviceLink/SDLRTPH264Packetizer.h | 47 | ||||
-rw-r--r-- | SmartDeviceLink/SDLRTPH264Packetizer.m | 183 | ||||
-rw-r--r-- | SmartDeviceLinkTests/DevAPISpecs/SDLRTPH264PacketizerSpec.m | 401 |
4 files changed, 643 insertions, 0 deletions
diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj index 47c11653b..4b850a60c 100644 --- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj +++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj @@ -1044,6 +1044,9 @@ EED5CA001F4D18DC00F04000 /* SDLH264ByteStreamPacketizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EED5C9FF1F4D18DC00F04000 /* SDLH264ByteStreamPacketizer.h */; }; EED5CA021F4D18EC00F04000 /* SDLH264ByteStreamPacketizer.m in Sources */ = {isa = PBXBuildFile; fileRef = EED5CA011F4D18EC00F04000 /* SDLH264ByteStreamPacketizer.m */; }; EED5CA041F4D1D5E00F04000 /* SDLH264ByteStreamPacketizerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = EED5CA031F4D1D5E00F04000 /* SDLH264ByteStreamPacketizerSpec.m */; }; + EED5CA061F4D1E2300F04000 /* SDLRTPH264Packetizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EED5CA051F4D1E2300F04000 /* SDLRTPH264Packetizer.h */; }; + EED5CA081F4D1E2E00F04000 /* SDLRTPH264Packetizer.m in Sources */ = {isa = PBXBuildFile; fileRef = EED5CA071F4D1E2E00F04000 /* SDLRTPH264Packetizer.m */; }; + EED5CA0A1F4D206800F04000 /* SDLRTPH264PacketizerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = EED5CA091F4D206800F04000 /* SDLRTPH264PacketizerSpec.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2173,6 +2176,9 @@ EED5C9FF1F4D18DC00F04000 /* SDLH264ByteStreamPacketizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDLH264ByteStreamPacketizer.h; sourceTree = "<group>"; }; EED5CA011F4D18EC00F04000 /* SDLH264ByteStreamPacketizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLH264ByteStreamPacketizer.m; sourceTree = "<group>"; }; EED5CA031F4D1D5E00F04000 /* SDLH264ByteStreamPacketizerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLH264ByteStreamPacketizerSpec.m; path = DevAPISpecs/SDLH264ByteStreamPacketizerSpec.m; sourceTree = "<group>"; }; + EED5CA051F4D1E2300F04000 /* SDLRTPH264Packetizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDLRTPH264Packetizer.h; sourceTree = "<group>"; }; + EED5CA071F4D1E2E00F04000 /* SDLRTPH264Packetizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLRTPH264Packetizer.m; sourceTree = "<group>"; }; + EED5CA091F4D206800F04000 /* SDLRTPH264PacketizerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDLRTPH264PacketizerSpec.m; path = DevAPISpecs/SDLRTPH264PacketizerSpec.m; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -4159,6 +4165,7 @@ children = ( DA8966EE1E5693E300413EAB /* SDLStreamingMediaLifecycleManagerSpec.m */, EED5CA031F4D1D5E00F04000 /* SDLH264ByteStreamPacketizerSpec.m */, + EED5CA091F4D206800F04000 /* SDLRTPH264PacketizerSpec.m */, ); name = Streaming; sourceTree = "<group>"; @@ -4183,6 +4190,8 @@ EED5C9FD1F4D18D100F04000 /* SDLH264Packetizer.h */, EED5C9FF1F4D18DC00F04000 /* SDLH264ByteStreamPacketizer.h */, EED5CA011F4D18EC00F04000 /* SDLH264ByteStreamPacketizer.m */, + EED5CA051F4D1E2300F04000 /* SDLRTPH264Packetizer.h */, + EED5CA071F4D1E2E00F04000 /* SDLRTPH264Packetizer.m */, ); name = "Video Encoding"; sourceTree = "<group>"; @@ -4504,6 +4513,7 @@ 5D61FCE41A84238C00846EE7 /* SDLKeyboardProperties.h in Headers */, 5D61FDED1A84238C00846EE7 /* SDLUnsubscribeVehicleDataResponse.h in Headers */, 5D4631041F2120A30092EFDC /* SDLControlFramePayloadType.h in Headers */, + EED5CA061F4D1E2300F04000 /* SDLRTPH264Packetizer.h in Headers */, 5D61FCCF1A84238C00846EE7 /* SDLImageField.h in Headers */, 5D535DC51B72473800CF7760 /* SDLGlobals.h in Headers */, 5D79A03B1CE36F030035797B /* SDLUploadFileOperation.h in Headers */, @@ -5135,6 +5145,7 @@ 5D61FC2C1A84238C00846EE7 /* SDLAbstractTransport.m in Sources */, 5D61FD8E1A84238C00846EE7 /* SDLSetMediaClockTimerResponse.m in Sources */, 5D61FD721A84238C00846EE7 /* SDLRPCRequest.m in Sources */, + EED5CA081F4D1E2E00F04000 /* SDLRTPH264Packetizer.m in Sources */, 5D61FDF01A84238C00846EE7 /* SDLUpdateMode.m in Sources */, 5D61FC931A84238C00846EE7 /* SDLDisplayType.m in Sources */, 5D61FCE31A84238C00846EE7 /* SDLKeyboardLayout.m in Sources */, @@ -5380,6 +5391,7 @@ 5DA23FF81F2FAF2D009C0313 /* SDLControlFramePayloadRPCStartServiceAckSpec.m in Sources */, 162E83191A9BDE8B00906325 /* SDLOnLanguageChangeSpec.m in Sources */, 5DB1BCDD1D243DC3002FFC37 /* SDLLifecycleConfigurationSpec.m in Sources */, + EED5CA0A1F4D206800F04000 /* SDLRTPH264PacketizerSpec.m in Sources */, 162E83611A9BDE8B00906325 /* SDLSetAppIconResponseSpec.m in Sources */, 162E83471A9BDE8B00906325 /* SDLUnsubscribeVehicleDataSpec.m in Sources */, 162E839A1A9BDE8B00906325 /* SDLRPCMessageSpec.m in Sources */, diff --git a/SmartDeviceLink/SDLRTPH264Packetizer.h b/SmartDeviceLink/SDLRTPH264Packetizer.h new file mode 100644 index 000000000..c5a76bc3c --- /dev/null +++ b/SmartDeviceLink/SDLRTPH264Packetizer.h @@ -0,0 +1,47 @@ +// +// SDLRTPH264Packetizer.h +// SmartDeviceLink-iOS +// +// Created by Sho Amano on 4/11/17. +// Copyright © 2017 Xevo Inc. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import "SDLH264Packetizer.h" + +@interface SDLRTPH264Packetizer : NSObject <SDLH264Packetizer> + +/** + * Payload Type (PT) of RTP header field. + * + * @note This must be between 0 and 127. Default value is 96. + */ +@property (assign, nonatomic) UInt8 payloadType; + +/** + * SSRC of RTP header field. + * + * @note A random value is generated and used as default. + */ +@property (assign, nonatomic) UInt32 ssrc; + +/** + * Initializer. + */ +- (instancetype)init; + +/** + * Creates RTP packets from given H.264 NAL units and presentation timestamp. + * + * @param nalUnits List of NAL units to create packets. + * @param pts Presentation timestamp associated to the NAL units, in seconds. + * + * @return List of NSData. Each NSData holds a RTP packet. + * + * @note This method cannot be called more than once with same pts value. + * All NAL units that belongs to a frame should be included in + * nalUnits array. + */ +- (NSArray *)createPackets:(NSArray *)nalUnits pts:(double)pts; + +@end diff --git a/SmartDeviceLink/SDLRTPH264Packetizer.m b/SmartDeviceLink/SDLRTPH264Packetizer.m new file mode 100644 index 000000000..31934c4fa --- /dev/null +++ b/SmartDeviceLink/SDLRTPH264Packetizer.m @@ -0,0 +1,183 @@ +// +// SDLRTPH264Packetizer.m +// SmartDeviceLink-iOS +// +// Created by Sho Amano on 4/11/17. +// Copyright © 2017 Xevo Inc. All rights reserved. +// + +/* + * Note for testing. + * The RTP stream generated by this packetizer can be tested with GStreamer (1.4 or later). + * Assuming that "VideoStreamPort" is configured as 5050 in smartDeviceLink.ini, here is the + * GStreamer pipeline that receives the stream, decode it and render it: + * + * $ gst-launch-1.0 souphttpsrc location=http://127.0.0.1:5050 ! "application/x-rtp-stream" ! rtpstreamdepay ! "application/x-rtp,media=(string)video,clock-rate=90000,encoding-name=(string)H264" ! rtph264depay ! "video/x-h264, stream-format=(string)avc, alignment=(string)au" ! avdec_h264 ! autovideosink sync=false + */ + +#import <Foundation/Foundation.h> +#import "SDLRTPH264Packetizer.h" +#import "SDLLogMacros.h" + +const NSUInteger FRAME_LENGTH_LEN = 2; +const NSUInteger MAX_RTP_PACKET_SIZE = 65535; // because length field is two bytes (RFC 4571) +const NSUInteger RTP_HEADER_LEN = 12; +const UInt8 DEFAULT_PAYLOAD_TYPE = 96; +const NSUInteger FU_INDICATOR_LEN = 1; +const NSUInteger FU_HEADER_LEN = 1; +const UInt8 TYPE_FU_A = 0x1C; +const NSUInteger CLOCK_RATE = 90000; // we use 90 kHz clock rate ([5.1] in RFC 6184) + +// write 2-byte value in network byte order +static inline void writeShortInNBO(UInt8 *p, UInt16 value) { + p[0] = (value >> 8) & 0xFF; + p[1] = value & 0xFF; +} + +// write 4-byte value in network byte order +static inline void writeLongInNBO(UInt8 *p, UInt32 value) { + p[0] = (value >> 24) & 0xFF; + p[1] = (value >> 16) & 0xFF; + p[2] = (value >> 8) & 0xFF; + p[3] = value & 0xFF; +} + +@interface SDLRTPH264Packetizer () <SDLH264Packetizer> +@property (assign, nonatomic) UInt32 initialTimestamp; +@property (assign, nonatomic) UInt16 sequenceNum; +@end + +@implementation SDLRTPH264Packetizer + +- (instancetype)init { + if (self = [super init]) { + _payloadType = DEFAULT_PAYLOAD_TYPE; + // initial value of the sequence number and timestamp should be random ([5.1] in RFC3550) + _initialTimestamp = arc4random_uniform(UINT32_MAX); + _sequenceNum = (UInt16)arc4random_uniform(UINT16_MAX); + _ssrc = arc4random_uniform(UINT32_MAX); + } + + return self; +} + +- (void)setPayloadType:(UInt8)payloadType { + if (payloadType <= 127) { + _payloadType = payloadType; + } else { + _payloadType = DEFAULT_PAYLOAD_TYPE; + } +} + +- (NSArray *)createPackets:(NSArray *)nalUnits pts:(double)pts { + NSMutableArray *rtpFrames = [NSMutableArray array]; + NSUInteger nalUnitsCount = [nalUnits count]; + + for (NSUInteger i = 0; i < nalUnitsCount; i++) { + NSData *nalUnit = nalUnits[i]; + NSUInteger nalUnitLength = [nalUnit length]; + BOOL isLast = (i + 1) == nalUnitsCount; + + if (RTP_HEADER_LEN + nalUnitLength > MAX_RTP_PACKET_SIZE) { + // Split into multiple Fragmentation Units ([5.8] in RFC 6184) + UInt8 firstByte; + [nalUnit getBytes:&firstByte length:1]; + BOOL isFirstFragment = YES; + BOOL isLastFragment = NO; + NSUInteger offset = 1; // we have already read the first byte + + while (offset < nalUnitLength) { + NSUInteger payloadLength = MAX_RTP_PACKET_SIZE - (RTP_HEADER_LEN + FU_INDICATOR_LEN + FU_HEADER_LEN); + if (nalUnitLength - offset <= payloadLength) { + payloadLength = nalUnitLength - offset; + isLastFragment = YES; + } + NSUInteger packetSize = RTP_HEADER_LEN + FU_INDICATOR_LEN + FU_HEADER_LEN + payloadLength; + NSUInteger frameSize = FRAME_LENGTH_LEN + packetSize; + + UInt8 *buffer = malloc(frameSize); + if (buffer == NULL) { + SDLLogE(@"malloc() error"); + return nil; + } + UInt8 *p = buffer; + + p += [self writeFrameHeader:p packetSize:packetSize]; + p += [self writeRTPHeader:p marker:isLast pts:pts]; + + // FU indicator + *p++ = (firstByte & 0xE0) | TYPE_FU_A; + // FU header + *p++ = (isFirstFragment ? 0x80 : isLastFragment ? 0x40 : 0) | (firstByte & 0x1F); + // FU payload + [nalUnit getBytes:p range:NSMakeRange(offset, payloadLength)]; + offset += payloadLength; + + NSData *rtpFrame = [NSData dataWithBytesNoCopy:buffer length:frameSize]; + [rtpFrames addObject:rtpFrame]; + + isFirstFragment = NO; + } + } else { + // Use Single NAL Unit Packet ([5.6] in RFC 6184) + NSUInteger packetSize = RTP_HEADER_LEN + nalUnitLength; + NSUInteger frameSize = FRAME_LENGTH_LEN + packetSize; + + UInt8 *buffer = malloc(frameSize); + if (buffer == NULL) { + SDLLogE(@"malloc() error"); + return nil; + } + UInt8 *p = buffer; + + p += [self writeFrameHeader:p packetSize:packetSize]; + p += [self writeRTPHeader:p marker:isLast pts:pts]; + [nalUnit getBytes:p length:nalUnitLength]; + + NSData *rtpFrame = [NSData dataWithBytesNoCopy:buffer length:frameSize]; + [rtpFrames addObject:rtpFrame]; + } + } + + return rtpFrames; +} + +/** + * Write RTP Frame header (length field) to supplied buffer. + * + * @param p the buffer in which a header is written. + * @param packetSize size of a RTP packet that follows to this frame header. + * + * @return number of bytes written, which is always 2. + */ +- (NSUInteger)writeFrameHeader:(UInt8 *)p packetSize:(NSUInteger)packetSize { + NSAssert(packetSize <= MAX_RTP_PACKET_SIZE, @"RTP packet is too big"); + writeShortInNBO(p, (UInt16)packetSize); + return FRAME_LENGTH_LEN; +} + +/** + * Write RTP header to supplied buffer. + * + * @param p the buffer in which a header is written. + * @param isLast whether this is the last packet of an access unit. + * @param pts presentation timestamp in seconds. + * + * @return number of bytes written, which is always 12. + */ +- (NSUInteger)writeRTPHeader:(UInt8 *)p marker:(BOOL)isLast pts:(double)pts { + UInt32 ptsIn90kHz = pts * CLOCK_RATE; + + // Version = 2, Padding = 0, Extension = 0, CSRC count = 0 + p[0] = 0x80; + // Marker = isLast, Payload type = self.payloadType + p[1] = (isLast ? 0x80 : 0) | (self.payloadType & 0x7F); + writeShortInNBO(p + 2, self.sequenceNum); + writeLongInNBO(p + 4, self.initialTimestamp + ptsIn90kHz); + writeLongInNBO(p + 8, self.ssrc); + + self.sequenceNum++; + return RTP_HEADER_LEN; +} + +@end diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLRTPH264PacketizerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLRTPH264PacketizerSpec.m new file mode 100644 index 000000000..9f028bc3a --- /dev/null +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLRTPH264PacketizerSpec.m @@ -0,0 +1,401 @@ +// +// SDLRTPH264PacketizerSpec.m +// SmartDeviceLink-iOS +// +// Created by Sho Amano on 4/13/17. +// Copyright © 2017 Xevo Inc. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import <Quick/Quick.h> +#import <Nimble/Nimble.h> +#import "SDLRTPH264Packetizer.h" + +// read 2-byte in network byte order and convert it to a UInt16 +static inline UInt16 readShortInNBO(const UInt8 *p) { + return (p[0] << 8) | p[1]; +} + +// read 4-byte in network byte order and convert it to a UInt32 +static inline UInt32 readLongInNBO(const UInt8 *p) { + return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; +} + +QuickSpecBegin(SDLRTPH264PacketizerSpec) + +describe(@"a RTP H264 packetizer", ^{ + // sample NAL units (SPS, PPS, I-frame, P-frame) + const UInt8 spsData[] = {0x67, 0x42, 0xC0, 0x0A, 0xA6, 0x11, 0x11, 0xE8, 0x40, 0x00, 0x00, 0xFA, 0x40, 0x00, 0x3A, 0x98, 0x23, 0xC4, 0x89, 0x84, 0x60}; + const UInt8 ppsData[] = {0x68, 0xC8, 0x42, 0x0F, 0x13, 0x20}; + const UInt8 iframeData[] = {0x65, 0x88, 0x82, 0x07, 0x67, 0x39, 0x31, 0x40, 0x00, 0x5E, 0x0A, 0xFB, 0xEF, 0xAE, 0xBA, 0xEB, 0xAE, 0xBA, 0xEB, 0xC0}; + const UInt8 pframeData[] = {0x41, 0x9A, 0x1C, 0x0E, 0xCE, 0x71, 0xB0}; + + NSData *sps = [NSData dataWithBytes:spsData length:sizeof(spsData)]; + NSData *pps = [NSData dataWithBytes:ppsData length:sizeof(ppsData)]; + NSData *iframe = [NSData dataWithBytes:iframeData length:sizeof(iframeData)]; + NSData *pframe = [NSData dataWithBytes:pframeData length:sizeof(pframeData)]; + + const NSUInteger FRAME_LENGTH_LEN = 2; + const NSUInteger MAX_RTP_PACKET_SIZE = 65535; + const NSUInteger RTP_HEADER_LEN = 12; + const UInt8 DEFAULT_PAYLOAD_TYPE = 96; + const UInt8 TYPE_FU_A = 0x1C; + const NSUInteger CLOCK_RATE = 90000; + + __block SDLRTPH264Packetizer *packetizer = nil; + + beforeEach(^{ + packetizer = [[SDLRTPH264Packetizer alloc] init]; + }); + + describe(@"its output array", ^{ + it(@"has same number or more elements compared to the input NAL units", ^{ + NSArray *nalUnits = @[sps, pps, iframe]; + NSArray *results = [packetizer createPackets:nalUnits pts:0.0]; + expect(@([results count])).to(beGreaterThanOrEqualTo(@([nalUnits count]))); + }); + }); + + describe(@"First two bytes of its output", ^{ + it(@"indicates the length of a RTP packet", ^{ + NSArray *nalUnits = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits pts:0.0]; + const UInt8 *header = [results[0] bytes]; + UInt16 length = readShortInNBO(header); + expect(@((length))).to(equal(@([results[0] length] - FRAME_LENGTH_LEN))); + }); + }); + + describe(@"header of the RTP packet", ^{ + __block const UInt8 *header; + + beforeEach(^{ + NSArray *nalUnits = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits pts:0.0]; + header = [results[0] bytes]; + }); + + it(@"indicates version 2", ^{ + expect(@((header[FRAME_LENGTH_LEN] >> 6) & 3)).to(equal(@2)); + }); + it(@"indicates no padding", ^{ + expect(@((header[FRAME_LENGTH_LEN] >> 5) & 1)).to(equal(@0)); + }); + it(@"indicates no extension", ^{ + expect(@((header[FRAME_LENGTH_LEN] >> 4) & 1)).to(equal(@0)); + }); + it(@"indicates no CSRC", ^{ + expect(@(header[FRAME_LENGTH_LEN] & 0xF)).to(equal(@0)); + }); + }); + + describe(@"the marker bit in the header of the RTP packet", ^{ + context(@"when there is only one NAL unit input", ^{ + it(@"is always set", ^{ + NSArray *nalUnits1 = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + const UInt8 *header = [results[0] bytes]; + expect(@((header[FRAME_LENGTH_LEN+1] >> 7) & 1)).to(equal(@1)); + + NSArray *nalUnits2 = @[pframe]; + results = [packetizer createPackets:nalUnits2 pts:1.0/30]; + header = [results[0] bytes]; + expect(@((header[FRAME_LENGTH_LEN+1] >> 7) & 1)).to(equal(@1)); + }); + }); + + context(@"when multiple NAL units are input for an access unit", ^{ + it(@"is set only for the last packet", ^{ + // 3 NAL units for a frame + NSArray *nalUnits1 = @[sps, pps, iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + + [results enumerateObjectsUsingBlock:^(NSData *packet, NSUInteger index, BOOL *stop) { + const UInt8 *header = [packet bytes]; + if (index == [results count] - 1) { + expect(@((header[FRAME_LENGTH_LEN+1] >> 7) & 1)).to(equal(@1)); + } else { + expect(@((header[FRAME_LENGTH_LEN+1] >> 7) & 1)).to(equal(@0)); + } + }]; + + // Only 1 NAL unit for the next frame + NSArray *nalUnits2 = @[pframe]; + results = [packetizer createPackets:nalUnits2 pts:1.0/30]; + const UInt8 *header = [results[0] bytes]; + expect(@((header[FRAME_LENGTH_LEN+1] >> 7) & 1)).to(equal(@1)); + }); + }); + }); + + describe(@"the payload type in the header of the RTP packet", ^{ + context(@"when it is not configured", ^{ + it(@"equals to 96", ^{ + NSArray *nalUnits1 = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + const UInt8 *header = [results[0] bytes]; + expect(@(header[FRAME_LENGTH_LEN+1] & 0x7F)).to(equal(@(DEFAULT_PAYLOAD_TYPE))); + }); + }); + + context(@"when it is explicitly configured", ^{ + it(@"is same as the given number if the number is between 0 and 127", ^{ + UInt8 payloadType = 100; + packetizer.payloadType = payloadType; + + NSArray *nalUnits1 = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + const UInt8 *header = [results[0] bytes]; + expect(@(header[FRAME_LENGTH_LEN+1] & 0x7F)).to(equal(@(payloadType))); + }); + + it(@"equals to 96 if the given number is out of range", ^{ + packetizer.payloadType = 200; + + NSArray *nalUnits1 = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + const UInt8 *header = [results[0] bytes]; + expect(@(header[FRAME_LENGTH_LEN+1] & 0x7F)).to(equal(@(DEFAULT_PAYLOAD_TYPE))); + }); + }); + }); + + describe(@"the sequence number in the header of the RTP packet", ^{ + it(@"has an initial value of random number", ^{ + // no way to test a random number + }); + + it(@"increments by one for the next packet", ^{ + NSArray *nalUnits1 = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + const UInt8 *header = [results[0] bytes]; + UInt16 seqNum = readShortInNBO(&header[FRAME_LENGTH_LEN+2]); + + NSArray *nalUnits2 = @[pframe]; + results = [packetizer createPackets:nalUnits2 pts:1.0/30]; + header = [results[0] bytes]; + + seqNum++; + expect(@(seqNum)).to(equal(@(readShortInNBO(&header[FRAME_LENGTH_LEN+2])))); + }); + + it(@"wraps around after reaching 65535", ^{ + NSArray *nalUnits = @[iframe]; + UInt16 prevSeqNum = 0; + + for (NSUInteger i = 0; i <= 65536; i++) { + NSArray *results = [packetizer createPackets:nalUnits pts:i/30.0]; + const UInt8 *header = [results[0] bytes]; + UInt16 seqNum = readShortInNBO(&header[FRAME_LENGTH_LEN+2]); + + if (prevSeqNum == 65535) { + expect(@(seqNum)).to(equal(@(0))); + break; // end testing + } else { + prevSeqNum = seqNum; + } + } + }); + }); + + describe(@"the timestamp field in the header of the RTP packet", ^{ + it(@"has an initial value of random number", ^{ + // no way to test a random number + }); + + it(@"then increases in 90 kHz clock value", ^{ + NSArray *nalUnits = @[iframe]; + UInt32 initialPTS = 0; + + for (NSUInteger i = 0; i <= 100; i++) { + // the timestamp increases by 1/30 seconds + NSArray *results = [packetizer createPackets:nalUnits pts:i/30.0]; + const UInt8 *header = [results[0] bytes]; + UInt32 pts = readLongInNBO(&header[FRAME_LENGTH_LEN+4]); + + if (i == 0) { + initialPTS = pts; + } else { + UInt32 expectedPTS = initialPTS + i / 30.0 * CLOCK_RATE; + // accept calculation error (+-1) + expect(@(pts)).to(beGreaterThanOrEqualTo(@(expectedPTS - 1))); + expect(@(pts)).to(beLessThanOrEqualTo(@(expectedPTS + 1))); + } + } + }); + }); + + describe(@"the SSRC field in the header of the RTP packet", ^{ + context(@"when it is not configured", ^{ + it(@"is a random number", ^{ + // No way to test a random number. We only check that it is shared among packets. + NSArray *nalUnits1 = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + const UInt8 *header = [results[0] bytes]; + UInt32 ssrc = readLongInNBO(&header[FRAME_LENGTH_LEN+8]); + + NSArray *nalUnits2 = @[pframe]; + results = [packetizer createPackets:nalUnits2 pts:1.0/30]; + header = [results[0] bytes]; + UInt32 ssrc2 = readLongInNBO(&header[FRAME_LENGTH_LEN+8]); + + expect(@(ssrc)).to(equal(@(ssrc2))); + }); + }); + + context(@"when it is explicitly configured", ^{ + it(@"is same as the given number", ^{ + UInt32 expectedSSRC = 0xFEDCBA98; + packetizer.ssrc = expectedSSRC; + + NSArray *nalUnits1 = @[iframe]; + NSArray *results = [packetizer createPackets:nalUnits1 pts:0.0]; + const UInt8 *header = [results[0] bytes]; + UInt32 ssrc = readLongInNBO(&header[FRAME_LENGTH_LEN+8]); + expect(@(ssrc)).to(equal(@(expectedSSRC))); + + NSArray *nalUnits2 = @[pframe]; + results = [packetizer createPackets:nalUnits2 pts:1.0/30]; + header = [results[0] bytes]; + ssrc = readLongInNBO(&header[FRAME_LENGTH_LEN+8]); + expect(@(ssrc)).to(equal(@(expectedSSRC))); + }); + }); + }); + + describe(@"the payload of its output packet", ^{ + NSData *(^createFakeNALUnit)(UInt8, NSUInteger) = ^NSData *(UInt8 firstByte, NSUInteger length) { + UInt8 *data = malloc(length); + data[0] = firstByte; + for (NSUInteger i = 1; i < length; i++) { + data[i] = i % 256; + } + return [NSData dataWithBytes:data length:length]; + }; + + UInt8 firstByte; + [iframe getBytes:&firstByte length:1]; + + it(@"is not fragmented if input NAL unit size is less than 65524 bytes (65536-12)", ^{ + NSData *fakeNALUnit = createFakeNALUnit(firstByte, MAX_RTP_PACKET_SIZE - RTP_HEADER_LEN); + NSArray *nalUnits = @[fakeNALUnit]; + NSArray *results = [packetizer createPackets:nalUnits pts:0.0]; + + // we should get only one packet + expect(@([results count])).to(equal(@(1))); + }); + + it(@"is fragmented if input NAL unit size equals to or is greater than 65524 bytes", ^{ + NSData *fakeNALUnit = createFakeNALUnit(firstByte, MAX_RTP_PACKET_SIZE - RTP_HEADER_LEN + 1); + NSArray *nalUnits = @[fakeNALUnit]; + NSArray *results = [packetizer createPackets:nalUnits pts:0.0]; + + // the NAL unit should be fragmented into two + expect(@([results count])).to(equal(@(2))); + }); + + context(@"when payload is not fragmented", ^{ + it(@"is duplicate of input NAL unit", ^{ + NSArray *nalUnits = @[sps, pps, iframe]; + NSArray *results = [packetizer createPackets:nalUnits pts:0.0]; + + NSUInteger nalUnitIndex = 0; + for (NSData *packet in results) { + const UInt8 *p = [packet bytes]; + int ret = memcmp(p + FRAME_LENGTH_LEN + RTP_HEADER_LEN, + [nalUnits[nalUnitIndex] bytes], + [nalUnits[nalUnitIndex] length]); + expect(@(ret)).to(equal(@0)); + nalUnitIndex++; + } + }); + }); + + context(@"when payload is fragmented", ^{ + __block NSData *fakeNALUnit; + __block NSArray *results; + + beforeEach(^{ + fakeNALUnit = createFakeNALUnit(firstByte, MAX_RTP_PACKET_SIZE - RTP_HEADER_LEN + 1); + NSArray *nalUnits = @[fakeNALUnit]; + results = [packetizer createPackets:nalUnits pts:0.0]; + }); + + describe(@"its first byte", ^{ + it(@"has F bit and NRI field which are same as those of the input NAL unit", ^{ + for (NSData *packet in results) { + const UInt8 *header = [packet bytes]; + expect(@((header[FRAME_LENGTH_LEN+RTP_HEADER_LEN] >> 5) & 3)).to(equal(@((firstByte >> 5) & 3))); + } + }); + + it(@"indicates a FU-A type (0x1C)", ^{ + for (NSData *packet in results) { + const UInt8 *header = [packet bytes]; + expect(@(header[FRAME_LENGTH_LEN+RTP_HEADER_LEN] & 0x1F)).to(equal(@(TYPE_FU_A))); + } + }); + }); + + describe(@"its second byte", ^{ + it(@"has a start bit which is set only at the beginning of fragment group", ^{ + BOOL shouldBeFirstFragment = YES; + + for (NSUInteger i = 0; i < [results count]; i++) { + const UInt8 *header = [results[i] bytes]; + UInt8 startBit = (header[FRAME_LENGTH_LEN+RTP_HEADER_LEN+1] >> 7) & 1; + expect(@(startBit)).to(equal(@(shouldBeFirstFragment ? 1 : 0))); + shouldBeFirstFragment = NO; + } + }); + + it(@"has a end bit which is set only at the end of fragment group", ^{ + BOOL shouldBeLastFragment = NO; + + for (NSUInteger i = 0; i < [results count]; i++) { + if (i == [results count] - 1) { + shouldBeLastFragment = YES; + } else { + shouldBeLastFragment = NO; + } + + const UInt8 *header = [results[i] bytes]; + UInt8 endBit = (header[FRAME_LENGTH_LEN+RTP_HEADER_LEN+1] >> 6) & 1; + expect(@(endBit)).to(equal(@(shouldBeLastFragment ? 1 : 0))); + } + }); + + it(@"has a reserved bit which is always zero", ^{ + for (NSUInteger i = 0; i < [results count]; i++) { + const UInt8 *header = [results[i] bytes]; + expect(@((header[FRAME_LENGTH_LEN+RTP_HEADER_LEN+1] >> 5) & 1)).to(equal(@(0))); + } + }); + + it(@"has a type field which is same as the input NAL unit's type", ^{ + for (NSUInteger i = 0; i < [results count]; i++) { + const UInt8 *header = [results[i] bytes]; + expect(@(header[FRAME_LENGTH_LEN+RTP_HEADER_LEN+1] & 0x1F)).to(equal(@(firstByte & 0x1F))); + } + }); + }); + + describe(@"its third and onward bytes", ^{ + it(@"equals to original NAL unit's second and onward bytes when concatenated", ^{ + NSMutableData *concatData = [[NSMutableData alloc] init]; + + for (NSUInteger i = 0; i < [results count]; i++) { + NSData *packet = results[i]; + const UInt8 *p = [packet bytes]; + [concatData appendBytes:p + FRAME_LENGTH_LEN + RTP_HEADER_LEN + 2 + length:[packet length] - (FRAME_LENGTH_LEN + RTP_HEADER_LEN + 2)]; + } + + expect(@([concatData isEqualToData:[fakeNALUnit subdataWithRange:NSMakeRange(1, [fakeNALUnit length] - 1)]])).to(beTruthy()); + }); + }); + }); + }); +}); + +QuickSpecEnd |