1c56b76c by David LaPalomento

Add support for program info descriptors

We were assuming that the program_info_descriptors field in the program mapping table was always of zero length and would end up misaligned reading the table entries if that metadata was present. Also added helper functions for testing to generate mp2t packets.
1 parent 8b4d830c
......@@ -4,24 +4,30 @@
FlvTag = videojs.hls.FlvTag,
H264Stream = videojs.hls.H264Stream,
AacStream = videojs.hls.AacStream,
m2tsPacketSize = 188;
console.assert(H264Stream);
console.assert(AacStream);
window.videojs.hls.SegmentParser = function() {
MP2T_PACKET_LENGTH,
STREAM_TYPES;
/**
* An object that incrementally transmuxes MPEG2 Trasport Stream
* chunks into an FLV.
*/
videojs.hls.SegmentParser = function() {
var
self = this,
parseTSPacket,
pmtPid,
streamBuffer = new Uint8Array(m2tsPacketSize),
streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH),
streamBufferByteCount = 0,
videoPid,
h264Stream = new H264Stream(),
audioPid,
aacStream = new AacStream(),
seekToKeyFrame = false;
// expose the stream metadata
self.stream = {
// the mapping between transport stream programs and the PIDs
// that form their elementary streams
programMapTable: {}
};
// For information on the FLV format, see
// http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
// Technically, this function returns the header and a metadata FLV tag
......@@ -146,24 +152,24 @@
// reconstruct the first packet. The rest of the packets will be
// parsed directly from data
if (streamBufferByteCount > 0) {
if (data.byteLength + streamBufferByteCount < m2tsPacketSize) {
if (data.byteLength + streamBufferByteCount < MP2T_PACKET_LENGTH) {
// the current data is less than a single m2ts packet, so stash it
// until we receive more
// ?? this seems to append streamBuffer onto data and then just give up. I'm not sure why that would be interesting.
videojs.log('data.length + streamBuffer.length < m2tsPacketSize ??');
videojs.log('data.length + streamBuffer.length < MP2T_PACKET_LENGTH ??');
streamBuffer.readBytes(data, data.length, streamBuffer.length);
return;
} else {
// we have enough data for an m2ts packet
// process it immediately
dataSlice = data.subarray(0, m2tsPacketSize - streamBufferByteCount);
dataSlice = data.subarray(0, MP2T_PACKET_LENGTH - streamBufferByteCount);
streamBuffer.set(dataSlice, streamBufferByteCount);
parseTSPacket(streamBuffer);
// reset the buffer
streamBuffer = new Uint8Array(m2tsPacketSize);
streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH);
streamBufferByteCount = 0;
}
}
......@@ -178,7 +184,7 @@
}
// base case: not enough data to parse a m2ts packet
if (data.byteLength - dataPosition < m2tsPacketSize) {
if (data.byteLength - dataPosition < MP2T_PACKET_LENGTH) {
if (data.byteLength - dataPosition > 0) {
// there are bytes remaining, save them for next time
streamBuffer.set(data.subarray(dataPosition),
......@@ -189,8 +195,8 @@
}
// attempt to parse a m2ts packet
if (parseTSPacket(data.subarray(dataPosition, dataPosition + m2tsPacketSize))) {
dataPosition += m2tsPacketSize;
if (parseTSPacket(data.subarray(dataPosition, dataPosition + MP2T_PACKET_LENGTH))) {
dataPosition += MP2T_PACKET_LENGTH;
} else {
// If there was an error parsing a TS packet. it could be
// because we are not TS packet aligned. Step one forward by
......@@ -201,24 +207,31 @@
}
};
/**
* Parses a video/mp2t packet and appends the underlying video and
* audio data onto h264stream and aacStream, respectively.
* @param data {Uint8Array} the bytes of an MPEG2-TS packet,
* including the sync byte.
* @return {boolean} whether a valid packet was encountered
*/
// TODO add more testing to make sure we dont walk past the end of a TS
// packet!
parseTSPacket = function(data) { // :ByteArray):Boolean {
var
offset = 0, // :uint
end = offset + m2tsPacketSize, // :uint
// Don't look for a sync byte. We handle that in
// parseSegmentBinaryData()
end = offset + MP2T_PACKET_LENGTH, // :uint
// Payload Unit Start Indicator
pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000
// PacketId
// packet identifier (PID), a unique identifier for the elementary
// stream this packet describes
pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111
// adaptation_field_control, whether this header is followed by an
// adaptation field, a payload, or both
afflag = (data[offset + 3] & 0x30 ) >>> 4,
aflen, // :uint
patTableId, // :int
patCurrentNextIndicator, // Boolean
patSectionLength, // :uint
......@@ -231,8 +244,8 @@
pts, // :uint
dts, // :uint
pmtTableId, // :int
pmtCurrentNextIndicator, // :Boolean
pmtProgramDescriptorsLength,
pmtSectionLength, // :uint
streamType, // :int
......@@ -243,42 +256,64 @@
// corrupt stream detection
// cc = (data[offset + 3] & 0x0F);
// Done with TS header
// move past the header
offset += 4;
if (afflag > 0x01) { // skip most of the adaption field
aflen = data[offset];
offset += aflen + 1;
// if an adaption field is present, its length is specified by
// the fifth byte of the PES header. The adaptation field is
// used to specify some forms of timing and control data that we
// do not currently use.
if (afflag > 0x01) {
offset += data[offset] + 1;
}
// Handle a Program Association Table (PAT). PATs map PIDs to
// individual programs. If this transport stream was being used
// for television broadcast, a program would probably be
// equivalent to a channel. In HLS, it would be very unusual to
// create an mp2t stream with multiple programs.
if (0x0000 === pid) {
// always test for PMT first! (becuse other variables default to 0)
// if pusi is set we must skip X bytes (PSI pointer field)
offset += pusi ? 1 + data[offset] : 0;
// The PAT may be split into multiple sections and those
// sections may be split into multiple packets. If a PAT
// section starts in this packet, PUSI will be true and the
// first byte of the playload will indicate the offset from
// the current position to the start of the section.
if (pusi) {
offset += 1 + data[offset];
}
patTableId = data[offset];
console.assert(0x00 === patTableId, 'patTableId should be 0x00');
if (patTableId !== 0x00) {
videojs.log('the table_id of the PAT should be 0x00 but was'
+ patTableId.toString(16));
}
// the current_next_indicator specifies whether this PAT is
// currently applicable or is part of the next table to become
// active
patCurrentNextIndicator = !!(data[offset + 5] & 0x01);
if (patCurrentNextIndicator) {
// section_length specifies the number of bytes following
// its position to the end of this section
patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
offset += 8; // skip past PSI header
// We currently only support streams with 1 program
patSectionLength = (patSectionLength - 9) / 4;
if (1 !== patSectionLength) {
// move past the rest of the PSI header to the first program
// map table entry
offset += 8;
// we don't handle streams with more than one program, so
// raise an exception if we encounter one
// section_length = rest of header + (n * entry length) + CRC
// = 5 + (n * 4) + 4
if ((patSectionLength - 5 - 4) / 4 !== 1) {
throw new Error("TS has more that 1 program");
}
// if we ever support more that 1 program (unlikely) loop over them here
// var programNumber = data[offset + 0] << 8 | data[offset + 1];
// var programId = (data[offset+2] & 0x1F) << 8 | data[offset + 3];
pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
// the Program Map Table (PMT) associates the underlying
// video and audio streams with a unique PID
self.stream.pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
}
// We could test the CRC here to detect corruption with extra CPU cost
} else if (videoPid === pid || audioPid === pid) {
} else if (pid === self.stream.programMapTable[STREAM_TYPES.h264] ||
pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
if (pusi) {
// comment out for speed
if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) {
......@@ -322,60 +357,81 @@
// Skip past "optional" portion of PTS header
offset += pesHeaderLength;
if (videoPid === pid) {
if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
// Stash this frame for future use.
// console.assert(videoFrames.length < 3);
h264Stream.setNextTimeStamp(pts,
dts,
dataAlignmentIndicator);
} else if (audioPid === pid) {
} else if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
aacStream.setNextTimeStamp(pts,
pesPacketSize,
dataAlignmentIndicator);
}
}
if (audioPid === pid) {
if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
aacStream.writeBytes(data, offset, end - offset);
} else if (videoPid === pid) {
} else if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
h264Stream.writeBytes(data, offset, end - offset);
}
} else if (pmtPid === pid) {
// TODO sanity check data[offset]
// if pusi is set we must skip X bytes (PSI pointer field)
offset += (pusi ? 1 + data[offset] : 0);
pmtTableId = data[offset];
console.assert(0x02 === pmtTableId);
} else if (self.stream.pmtPid === pid) {
// similarly to the PAT, jump to the first byte of the section
if (pusi) {
offset += 1 + data[offset];
}
if (data[offset] !== 0x02) {
videojs.log('The table_id of a PMT should be 0x02 but was '
+ data[offset].toString(16));
}
// whether this PMT is currently applicable or is part of the
// next table to become active
pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01);
if (pmtCurrentNextIndicator) {
audioPid = videoPid = 0;
pmtSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
// overwrite any existing program map table
self.stream.programMapTable = {};
// section_length specifies the number of bytes following
// its position to the end of this section
pmtSectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2];
// subtract the length of the program info descriptors
pmtProgramDescriptorsLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11];
pmtSectionLength -= pmtProgramDescriptorsLength;
// skip CRC and PSI data we dont care about
// rest of header + CRC = 9 + 4
pmtSectionLength -= 13;
offset += 12; // skip past PSI header and some PMT data
// align offset to the first entry in the PMT
offset += 12 + pmtProgramDescriptorsLength;
// iterate through the entries
while (0 < pmtSectionLength) {
// the type of data carried in the PID this entry describes
streamType = data[offset + 0];
// the PID for this entry
elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
offset += 5 + ESInfolength;
pmtSectionLength -= 5 + ESInfolength;
if (0x1B === streamType) {
if (0 !== videoPid) {
if (streamType === STREAM_TYPES.h264) {
if (self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 video stream");
}
videoPid = elementaryPID;
} else if (0x0F === streamType) {
if (0 !== audioPid) {
self.stream.programMapTable[streamType] = elementaryPID;
} else if (streamType === STREAM_TYPES.adts) {
if (self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 audio Stream");
}
audioPid = elementaryPID;
self.stream.programMapTable[streamType] = elementaryPID;
}
// TODO add support for MP3 audio
// the length of the entry descriptor
ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
// move to the first byte after the end of this entry
offset += 5 + ESInfolength;
pmtSectionLength -= 5 + ESInfolength;
}
}
// We could test the CRC here to detect corruption with extra CPU cost
......@@ -403,4 +459,12 @@
}
};
};
// MPEG2-TS constants
videojs.hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188;
videojs.hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = {
h264: 0x1b,
adts: 0x0f
};
})(window);
......
......@@ -26,6 +26,9 @@
0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
0x09, 0x00, 0x00, 0x00, 0x00
],
extend = videojs.util.mergeOptions,
testAudioTag,
testVideoTag,
testScriptTag,
......@@ -51,6 +54,144 @@
deepEqual(expectedHeader, header, 'the rest of the header is correct');
});
test('parses PMTs with program descriptors', function() {
var
makePmt = function(options) {
var
result = [],
entryCount = 0,
k,
sectionLength;
for (k in options.pids) {
entryCount++;
}
// table_id
result.push(0x02);
// section_syntax_indicator '0' reserved section_length
// 13 + (program_info_length) + (n * 5 + ES_info_length[n])
sectionLength = 13 + (5 * entryCount) + 17;
result.push(0x80 | (0xF00 & sectionLength >>> 8));
result.push(sectionLength & 0xFF);
// program_number
result.push(0x00);
result.push(0x01);
// reserved version_number current_next_indicator
result.push(0x01);
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
// reserved PCR_PID
result.push(0xe1);
result.push(0x00);
// reserved program_info_length
result.push(0xf0);
result.push(0x11); // hard-coded 17 byte descriptor
// program descriptors
result = result.concat([
0x25, 0x0f, 0xff, 0xff,
0x49, 0x44, 0x33, 0x20,
0xff, 0x49, 0x44, 0x33,
0x20, 0x00, 0x1f, 0x00,
0x01
]);
for (k in options.pids) {
// stream_type
result.push(options.pids[k]);
// reserved elementary_PID
result.push(0xe0 | (k & 0x1f00) >>> 8);
result.push(k & 0xff);
// reserved ES_info_length
result.push(0xf0);
result.push(0x00); // ES_info_length = 0
}
// CRC_32
result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
return result;
},
makePat = function(options) {
var
result = [],
k;
// table_id
result.push(0x00);
// section_syntax_indicator '0' reserved section_length
result.push(0x80);
result.push(0x0d); // section_length for one program
// transport_stream_id
result.push(0x00);
result.push(0x00);
// reserved version_number current_next_indicator
result.push(0x01); // current_next_indicator is 1
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
for (k in options.programs) {
// program_number
result.push((k & 0xFF00) >>> 8);
result.push(k & 0x00FF);
// reserved program_map_pid
result.push((options.programs[k] & 0x1f00) >>> 8);
result.push(options.programs[k] & 0xff);
}
return result;
},
makePsi = function(options) {
var result = [];
// pointer_field
if (options.payloadUnitStartIndicator) {
result.push(0x00);
}
if (options.programs) {
return result.concat(makePat(options));
}
return result.concat(makePmt(options));
},
makePacket = function(options) {
var
result = [],
settings = extend({
payloadUnitStartIndicator: true,
pid: 0x00
}, options),
pid;
// header
// sync_byte
result.push(0x47);
// transport_error_indicator payload_unit_start_indicator transport_priority PID
result.push((settings.pid & 0x1f) << 8 | 0x40);
result.push(settings.pid & 0xff);
// transport_scrambling_control adaptation_field_control continuity_counter
result.push(0x10);
result = result.concat(makePsi(settings));
// ensure the resulting packet is the correct size
result.length = videojs.hls.SegmentParser.MP2T_PACKET_LENGTH;
return result;
},
h264Type = videojs.hls.SegmentParser.STREAM_TYPES.h264,
adtsType = videojs.hls.SegmentParser.STREAM_TYPES.adts;
parser.parseSegmentBinaryData(new Uint8Array(makePacket({
programs: {
0x01: [0x01]
}
}).concat(makePacket({
pid: 0x01,
pids: {
0x02: h264Type, // h264 video
0x03: adtsType // adts audio
}
}))));
strictEqual(parser.stream.pmtPid, 0x01, 'PMT PID is 1');
strictEqual(parser.stream.programMapTable[h264Type], 0x02, 'video is PID 2');
strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3');
});
test('parses the first bipbop segment', function() {
parser.parseSegmentBinaryData(window.bcSegment);
......