1bbcbe8d by David LaPalomento

Migrate PSI parsing

Move the code that parses PMTs and PATs from segment parser into the re-organized transmuxing module. Add tests.
1 parent a13c2710
......@@ -7,7 +7,187 @@
(function(window, videojs, undefined) {
'use strict';
var Transmuxer = function() {
var PacketStream, ParseStream, Transmuxer, MP2T_PACKET_LENGTH;
MP2T_PACKET_LENGTH = 188; // bytes
/**
* Splits an incoming stream of binary data into MP2T packets.
*/
PacketStream = function() {
var
buffer = new Uint8Array(MP2T_PACKET_LENGTH),
end = 0;
PacketStream.prototype.init.call(this);
/**
* Deliver new bytes to the stream.
*/
this.push = function(bytes) {
var remaining, i;
// clear out any partial packets in the buffer
if (end > 0) {
remaining = MP2T_PACKET_LENGTH - end;
buffer.set(bytes.subarray(0, remaining), end);
// we still didn't write out a complete packet
if (bytes.byteLength < remaining) {
end += bytes.byteLength;
return;
}
bytes = bytes.subarray(remaining);
end = 0;
this.trigger('data', buffer);
}
// if less than a single packet is available, buffer it up for later
if (bytes.byteLength < MP2T_PACKET_LENGTH) {
buffer.set(bytes.subarray(i), end);
end += bytes.byteLength;
return;
}
// parse out all the completed packets
i = 0;
do {
this.trigger('data', bytes.subarray(i, i + MP2T_PACKET_LENGTH));
i += MP2T_PACKET_LENGTH;
remaining = bytes.byteLength - i;
} while (i < bytes.byteLength && remaining >= MP2T_PACKET_LENGTH);
// buffer any partial packets left over
if (remaining > 0) {
buffer.set(bytes.subarray(i));
end = remaining;
}
};
};
PacketStream.prototype = new videojs.Hls.Stream();
/**
* Accepts an MP2T PacketStream and emits data events with parsed
* forms of the individual packets.
*/
ParseStream = function() {
var parsePsi, parsePat, parsePmt, self;
PacketStream.prototype.init.call(this);
self = this;
this.programMapTable = {};
parsePsi = function(payload, psi) {
var offset = 0;
// PSI packets may be split into multiple sections and those
// sections may be split into multiple packets. If a PSI
// section starts in this packet, the payload_unit_start_indicator
// will be true and the first byte of the payload will indicate
// the offset from the current position to the start of the
// section.
if (psi.payloadUnitStartIndicator) {
offset += payload[offset] + 1;
}
if (psi.type === 'pat') {
parsePat(payload.subarray(offset), psi);
} else {
parsePmt(payload.subarray(offset), psi);
}
};
parsePat = function(payload, pat) {
// skip the PSI header and parse the first PMT entry
self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11];
pat.pmtPid = self.pmtPid;
};
/**
* Parse out the relevant fields of a Program Map Table (PMT).
* @param payload {Uint8Array} the PMT-specific portion of an MP2T
* packet. The first byte in this array should be the table_id
* field.
* @param pmt {object} the object that should be decorated with
* fields parsed from the PMT.
*/
parsePmt = function(payload, pmt) {
var tableEnd, programInfoLength, offset;
// PMTs can be sent ahead of the time when they should actually
// take effect. We don't believe this should ever be the case
// for HLS but we'll ignore "forward" PMT declarations if we see
// them. Future PMT declarations have the current_next_indicator
// set to zero.
if (!(payload[5] & 0x01)) {
return;
}
// overwrite any existing program map table
self.programMapTable = {};
// the mapping table ends right before the 32-bit CRC
tableEnd = payload.byteLength - 4;
// to determine where the table starts, we have to figure out how
// long the program info descriptors are
programInfoLength = (payload[10] & 0x0f) << 8 | payload[11];
// advance the offset to the first entry in the mapping table
offset = 12 + programInfoLength;
while (offset < tableEnd) {
// add an entry that maps the elementary_pid to the stream_type
self.programMapTable[(payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]] = payload[offset];
// move to the next table entry
// skip past the elementary stream descriptors, if present
offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5;
}
};
/**
* Deliver a new MP2T packet to the stream.
*/
this.push = function(packet) {
var
result = {},
offset = 4,
stream;
// make sure packet is aligned on a sync byte
if (packet[0] !== 0x47) {
return this.trigger('error', 'mis-aligned packet');
}
result.payloadUnitStartIndicator = !!(packet[1] & 0x40);
// pid is a 13-bit field starting at the last bit of packet[1]
result.pid = packet[1] & 0x1f;
result.pid <<= 8;
result.pid |= packet[2];
// 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 (((packet[3] & 0x30) >>> 4) > 0x01) {
offset += packet[offset] + 1;
}
// parse the rest of the packet based on the type
if (result.pid === 0) {
result.type = 'pat';
parsePsi(packet.subarray(offset), result);
} else if (result.pid === this.pmtPid) {
result.type = 'pmt';
parsePsi(packet.subarray(offset), result);
} else {
result.stream = this.programMapTable[result.pid];
result.type = 'pes';
}
this.trigger('data', result);
};
};
ParseStream.prototype = new videojs.Hls.Stream();
Transmuxer = function() {
Transmuxer.prototype.init.call(this);
this.push = function() {
this.mp4 = new Uint8Array();
......@@ -15,5 +195,13 @@ var Transmuxer = function() {
};
Transmuxer.prototype = new videojs.Hls.Stream();
window.videojs.Hls.Transmuxer = Transmuxer;
window.videojs.mp2t = {
PAT_PID: 0x0000,
MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH,
H264_STREAM_TYPE: 0x1b,
ADTS_STREAM_TYPE: 0x0f,
Transmuxer: Transmuxer,
PacketStream: PacketStream,
ParseStream: ParseStream
};
})(window, window.videojs);
......
......@@ -21,9 +21,250 @@
throws(block, [expected], [message])
*/
var
Transmuxer = videojs.Hls.Transmuxer,
PacketStream = videojs.mp2t.PacketStream,
packetStream,
ParseStream = videojs.mp2t.ParseStream,
parseStream,
Transmuxer = videojs.mp2t.Transmuxer,
transmuxer;
module('MP2T Packet Stream', {
setup: function() {
packetStream = new PacketStream();
}
});
test('empty input does not error', function() {
packetStream.push(new Uint8Array([]));
ok(true, 'did not throw');
});
test('parses a generic packet', function() {
var datas = [];
packetStream.on('data', function(event) {
datas.push(event);
});
packetStream.push(new Uint8Array(188));
equal(1, datas.length, 'fired one event');
equal(datas[0].byteLength, 188, 'delivered the packet');
});
test('buffers partial packets', function() {
var datas = [];
packetStream.on('data', function(event) {
datas.push(event);
});
packetStream.push(new Uint8Array(187));
equal(0, datas.length, 'did not fire an event');
packetStream.push(new Uint8Array(189));
equal(2, datas.length, 'fired events');
equal(188, datas[0].byteLength, 'parsed the first packet');
equal(188, datas[1].byteLength, 'parsed the second packet');
});
test('parses multiple packets delivered at once', function() {
var datas = [];
packetStream.on('data', function(event) {
datas.push(event);
});
packetStream.push(new Uint8Array(188 * 3));
equal(3, datas.length, 'fired three events');
equal(188, datas[0].byteLength, 'parsed the first packet');
equal(188, datas[1].byteLength, 'parsed the second packet');
equal(188, datas[2].byteLength, 'parsed the third packet');
});
test('buffers extra after multiple packets', function() {
var datas = [];
packetStream.on('data', function(event) {
datas.push(event);
});
packetStream.push(new Uint8Array(188 * 2 + 10));
equal(2, datas.length, 'fired two events');
equal(188, datas[0].byteLength, 'parsed the first packet');
equal(188, datas[1].byteLength, 'parsed the second packet');
packetStream.push(new Uint8Array(178));
equal(3, datas.length, 'fired a final event');
equal(188, datas[2].length, 'parsed the finel packet');
});
module('MP2T Parse Stream', {
setup: function() {
parseStream = new ParseStream();
}
});
test('emits an error on an invalid packet', function() {
var errors = [];
parseStream.on('error', function(error) {
errors.push(error);
});
parseStream.push(new Uint8Array(188));
equal(1, errors.length, 'emitted an error');
});
test('parses generic packet properties', function() {
var packet;
parseStream.on('data', function(data) {
packet = data;
});
parseStream.push(new Uint8Array([
0x47, // sync byte
// tei:0 pusi:1 tp:0 pid:0 0000 0000 0001 tsc:01 afc:10 cc:11 padding: 00
0x40, 0x01, 0x6c
]));
ok(packet.payloadUnitStartIndicator, 'parsed payload_unit_start_indicator');
ok(packet.pid, 'parsed PID');
});
test('parses a data packet with adaptation fields', function() {
var packet;
parseStream.on('data', function(data) {
packet = data;
});
parseStream.push(new Uint8Array([
0x47, // sync byte
// tei:0 pusi:1 tp:0 pid:0 0000 0000 0000 tsc:01 afc:10 cc:11 afl:00 0000 00 stuffing:00 0000 00 pscp:00 0001 padding:0000
0x40, 0x00, 0x6c, 0x00, 0x00, 0x10
]));
strictEqual(packet.type, 'pat', 'parsed the packet type');
});
test('parses a PES packet', function() {
var packet;
parseStream.on('data', function(data) {
packet = data;
});
// setup a program map table
parseStream.programMapTable = {
0x0010: videojs.mp2t.H264_STREAM_TYPE
};
parseStream.push(new Uint8Array([
0x47, // sync byte
// tei:0 pusi:1 tp:0 pid:0 0000 0000 0010 tsc:01 afc:01 cc:11 padding:00
0x40, 0x02, 0x5c
]));
strictEqual(packet.type, 'pes', 'parsed a PES packet');
});
test('parses packets with variable length adaptation fields and a payload', function() {
var packet;
parseStream.on('data', function(data) {
packet = data;
});
// setup a program map table
parseStream.programMapTable = {
0x0010: videojs.mp2t.H264_STREAM_TYPE
};
parseStream.push(new Uint8Array([
0x47, // sync byte
// tei:0 pusi:1 tp:0 pid:0 0000 0000 0010 tsc:01 afc:11 cc:11 afl:00 0000 11 stuffing:00 0000 0000 00 pscp:00 0001
0x40, 0x02, 0x7c, 0x0c, 0x00, 0x01
]));
strictEqual(packet.type, 'pes', 'parsed a PES packet');
});
/*
Packet Header:
| sb | tei pusi tp pid:5 | pid | tsc afc cc |
with af:
| afl | ... | <data> |
without af:
| <data> |
PAT:
| pf? | ... |
| tid | ssi '0' r sl:4 | sl | tsi:8 |
| tsi | r vn cni | sn | lsn |
with program_number == '0':
| pn | pn | r np:5 | np |
otherwise:
| pn | pn | r pmp:5 | pmp |
*/
test('parses the program map table pid from the program association table (PAT)', function() {
var packet;
parseStream.on('data', function(data) {
packet = data;
});
parseStream.push(new Uint8Array([
0x47, // sync byte
// tei:0 pusi:1 tp:0 pid:0 0000 0000 0000
0x40, 0x00,
// tsc:01 afc:01 cc:0000 pointer_field:0000 0000
0x50, 0x00,
// tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0000 0000
0x00, 0x00, 0x00,
// tsi:0000 0000 0000 0000
0x00, 0x00,
// r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000
0x01, 0x00, 0x00,
// pn:0000 0000 0000 0001
0x00, 0x01,
// r:000 pmp:0 0000 0010 0000
0x00, 0x10,
// crc32:0000 0000 0000 0000 0000 0000 0000 0000
0x00, 0x00, 0x00, 0x00
]));
strictEqual(0x0010, parseStream.pmtPid, 'parsed PMT pid');
});
test('parse the elementary streams from a program map table', function() {
var packet;
parseStream.on('data', function(data) {
packet = data;
});
parseStream.pmtPid = 0x0010;
parseStream.push(new Uint8Array([
0x47, // sync byte
// tei:0 pusi:1 tp:0 pid:0 0000 0010 0000
0x40, 0x10,
// tsc:01 afc:01 cc:0000 pointer_field:0000 0000
0x50, 0x00,
// tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0010 1111
0x00, 0x00, 0x2f,
// pn:0000 0000 0000 0001
0x00, 0x01,
// r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000
0x01, 0x00, 0x00,
// r:000 ppid:0 0011 1111 1111
0x03, 0xff,
// r:0000 pil:0000 0000 0000
0x00, 0x00,
// h264
// st:0001 1010 r:000 epid:0 0000 0001 0001
0x1b, 0x00, 0x11,
// r:0000 esil:0000 0000 0000
0x00, 0x00,
// adts
// st:0000 1111 r:000 epid:0 0000 0001 0010
0x0f, 0x00, 0x12,
// r:0000 esil:0000 0000 0000
0x00, 0x00,
// crc
0x00, 0x00, 0x00, 0x00
]));
ok(parseStream.programMapTable, 'parsed a program map');
strictEqual(0x1b, parseStream.programMapTable[0x11], 'associated h264 with pid 0x11');
strictEqual(0x0f, parseStream.programMapTable[0x12], 'associated adts with pid 0x12');
});
module('MP4 Transmuxer', {
setup: function() {
transmuxer = new Transmuxer();
......