summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Amano <samano@xevo.com>2017-04-14 20:21:55 +0900
committerSho Amano <samano@xevo.com>2017-08-23 15:11:36 +0900
commit0e5ec4087e6b1624a7815caf95acabf5c68b68ae (patch)
tree063211a626624781991f23eda18c8e74cdf67da6
parent07f9f71a56d79bab7493d3488c10da51ebd407aa (diff)
downloadsdl_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.pbxproj12
-rw-r--r--SmartDeviceLink/SDLRTPH264Packetizer.h47
-rw-r--r--SmartDeviceLink/SDLRTPH264Packetizer.m183
-rw-r--r--SmartDeviceLinkTests/DevAPISpecs/SDLRTPH264PacketizerSpec.m401
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