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 @@ ...@@ -7,7 +7,187 @@
7 (function(window, videojs, undefined) { 7 (function(window, videojs, undefined) {
8 'use strict'; 8 'use strict';
9 9
10 var Transmuxer = function() { 10 var PacketStream, ParseStream, Transmuxer, MP2T_PACKET_LENGTH;
11
12 MP2T_PACKET_LENGTH = 188; // bytes
13
14 /**
15 * Splits an incoming stream of binary data into MP2T packets.
16 */
17 PacketStream = function() {
18 var
19 buffer = new Uint8Array(MP2T_PACKET_LENGTH),
20 end = 0;
21
22 PacketStream.prototype.init.call(this);
23
24 /**
25 * Deliver new bytes to the stream.
26 */
27 this.push = function(bytes) {
28 var remaining, i;
29
30 // clear out any partial packets in the buffer
31 if (end > 0) {
32 remaining = MP2T_PACKET_LENGTH - end;
33 buffer.set(bytes.subarray(0, remaining), end);
34
35 // we still didn't write out a complete packet
36 if (bytes.byteLength < remaining) {
37 end += bytes.byteLength;
38 return;
39 }
40
41 bytes = bytes.subarray(remaining);
42 end = 0;
43 this.trigger('data', buffer);
44 }
45
46 // if less than a single packet is available, buffer it up for later
47 if (bytes.byteLength < MP2T_PACKET_LENGTH) {
48 buffer.set(bytes.subarray(i), end);
49 end += bytes.byteLength;
50 return;
51 }
52 // parse out all the completed packets
53 i = 0;
54 do {
55 this.trigger('data', bytes.subarray(i, i + MP2T_PACKET_LENGTH));
56 i += MP2T_PACKET_LENGTH;
57 remaining = bytes.byteLength - i;
58 } while (i < bytes.byteLength && remaining >= MP2T_PACKET_LENGTH);
59 // buffer any partial packets left over
60 if (remaining > 0) {
61 buffer.set(bytes.subarray(i));
62 end = remaining;
63 }
64 };
65 };
66 PacketStream.prototype = new videojs.Hls.Stream();
67
68 /**
69 * Accepts an MP2T PacketStream and emits data events with parsed
70 * forms of the individual packets.
71 */
72 ParseStream = function() {
73 var parsePsi, parsePat, parsePmt, self;
74 PacketStream.prototype.init.call(this);
75 self = this;
76
77 this.programMapTable = {};
78
79 parsePsi = function(payload, psi) {
80 var offset = 0;
81
82 // PSI packets may be split into multiple sections and those
83 // sections may be split into multiple packets. If a PSI
84 // section starts in this packet, the payload_unit_start_indicator
85 // will be true and the first byte of the payload will indicate
86 // the offset from the current position to the start of the
87 // section.
88 if (psi.payloadUnitStartIndicator) {
89 offset += payload[offset] + 1;
90 }
91
92 if (psi.type === 'pat') {
93 parsePat(payload.subarray(offset), psi);
94 } else {
95 parsePmt(payload.subarray(offset), psi);
96 }
97 };
98
99 parsePat = function(payload, pat) {
100 // skip the PSI header and parse the first PMT entry
101 self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11];
102 pat.pmtPid = self.pmtPid;
103 };
104
105 /**
106 * Parse out the relevant fields of a Program Map Table (PMT).
107 * @param payload {Uint8Array} the PMT-specific portion of an MP2T
108 * packet. The first byte in this array should be the table_id
109 * field.
110 * @param pmt {object} the object that should be decorated with
111 * fields parsed from the PMT.
112 */
113 parsePmt = function(payload, pmt) {
114 var tableEnd, programInfoLength, offset;
115
116 // PMTs can be sent ahead of the time when they should actually
117 // take effect. We don't believe this should ever be the case
118 // for HLS but we'll ignore "forward" PMT declarations if we see
119 // them. Future PMT declarations have the current_next_indicator
120 // set to zero.
121 if (!(payload[5] & 0x01)) {
122 return;
123 }
124
125 // overwrite any existing program map table
126 self.programMapTable = {};
127
128 // the mapping table ends right before the 32-bit CRC
129 tableEnd = payload.byteLength - 4;
130 // to determine where the table starts, we have to figure out how
131 // long the program info descriptors are
132 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11];
133
134 // advance the offset to the first entry in the mapping table
135 offset = 12 + programInfoLength;
136 while (offset < tableEnd) {
137 // add an entry that maps the elementary_pid to the stream_type
138 self.programMapTable[(payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]] = payload[offset];
139
140 // move to the next table entry
141 // skip past the elementary stream descriptors, if present
142 offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5;
143 }
144 };
145
146 /**
147 * Deliver a new MP2T packet to the stream.
148 */
149 this.push = function(packet) {
150 var
151 result = {},
152 offset = 4,
153 stream;
154 // make sure packet is aligned on a sync byte
155 if (packet[0] !== 0x47) {
156 return this.trigger('error', 'mis-aligned packet');
157 }
158 result.payloadUnitStartIndicator = !!(packet[1] & 0x40);
159
160 // pid is a 13-bit field starting at the last bit of packet[1]
161 result.pid = packet[1] & 0x1f;
162 result.pid <<= 8;
163 result.pid |= packet[2];
164
165 // if an adaption field is present, its length is specified by
166 // the fifth byte of the PES header. The adaptation field is
167 // used to specify some forms of timing and control data that we
168 // do not currently use.
169 if (((packet[3] & 0x30) >>> 4) > 0x01) {
170 offset += packet[offset] + 1;
171 }
172
173 // parse the rest of the packet based on the type
174 if (result.pid === 0) {
175 result.type = 'pat';
176 parsePsi(packet.subarray(offset), result);
177 } else if (result.pid === this.pmtPid) {
178 result.type = 'pmt';
179 parsePsi(packet.subarray(offset), result);
180 } else {
181 result.stream = this.programMapTable[result.pid];
182 result.type = 'pes';
183 }
184
185 this.trigger('data', result);
186 };
187 };
188 ParseStream.prototype = new videojs.Hls.Stream();
189
190 Transmuxer = function() {
11 Transmuxer.prototype.init.call(this); 191 Transmuxer.prototype.init.call(this);
12 this.push = function() { 192 this.push = function() {
13 this.mp4 = new Uint8Array(); 193 this.mp4 = new Uint8Array();
...@@ -15,5 +195,13 @@ var Transmuxer = function() { ...@@ -15,5 +195,13 @@ var Transmuxer = function() {
15 }; 195 };
16 Transmuxer.prototype = new videojs.Hls.Stream(); 196 Transmuxer.prototype = new videojs.Hls.Stream();
17 197
18 window.videojs.Hls.Transmuxer = Transmuxer; 198 window.videojs.mp2t = {
199 PAT_PID: 0x0000,
200 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH,
201 H264_STREAM_TYPE: 0x1b,
202 ADTS_STREAM_TYPE: 0x0f,
203 Transmuxer: Transmuxer,
204 PacketStream: PacketStream,
205 ParseStream: ParseStream
206 };
19 })(window, window.videojs); 207 })(window, window.videojs);
......
...@@ -21,9 +21,250 @@ ...@@ -21,9 +21,250 @@
21 throws(block, [expected], [message]) 21 throws(block, [expected], [message])
22 */ 22 */
23 var 23 var
24 Transmuxer = videojs.Hls.Transmuxer, 24 PacketStream = videojs.mp2t.PacketStream,
25 packetStream,
26 ParseStream = videojs.mp2t.ParseStream,
27 parseStream,
28 Transmuxer = videojs.mp2t.Transmuxer,
25 transmuxer; 29 transmuxer;
26 30
31 module('MP2T Packet Stream', {
32 setup: function() {
33 packetStream = new PacketStream();
34 }
35 });
36
37 test('empty input does not error', function() {
38 packetStream.push(new Uint8Array([]));
39 ok(true, 'did not throw');
40 });
41 test('parses a generic packet', function() {
42 var datas = [];
43 packetStream.on('data', function(event) {
44 datas.push(event);
45 });
46 packetStream.push(new Uint8Array(188));
47
48 equal(1, datas.length, 'fired one event');
49 equal(datas[0].byteLength, 188, 'delivered the packet');
50 });
51
52 test('buffers partial packets', function() {
53 var datas = [];
54 packetStream.on('data', function(event) {
55 datas.push(event);
56 });
57 packetStream.push(new Uint8Array(187));
58
59 equal(0, datas.length, 'did not fire an event');
60
61 packetStream.push(new Uint8Array(189));
62 equal(2, datas.length, 'fired events');
63 equal(188, datas[0].byteLength, 'parsed the first packet');
64 equal(188, datas[1].byteLength, 'parsed the second packet');
65 });
66
67 test('parses multiple packets delivered at once', function() {
68 var datas = [];
69 packetStream.on('data', function(event) {
70 datas.push(event);
71 });
72
73 packetStream.push(new Uint8Array(188 * 3));
74 equal(3, datas.length, 'fired three events');
75 equal(188, datas[0].byteLength, 'parsed the first packet');
76 equal(188, datas[1].byteLength, 'parsed the second packet');
77 equal(188, datas[2].byteLength, 'parsed the third packet');
78 });
79
80 test('buffers extra after multiple packets', function() {
81 var datas = [];
82 packetStream.on('data', function(event) {
83 datas.push(event);
84 });
85
86 packetStream.push(new Uint8Array(188 * 2 + 10));
87 equal(2, datas.length, 'fired two events');
88 equal(188, datas[0].byteLength, 'parsed the first packet');
89 equal(188, datas[1].byteLength, 'parsed the second packet');
90
91 packetStream.push(new Uint8Array(178));
92 equal(3, datas.length, 'fired a final event');
93 equal(188, datas[2].length, 'parsed the finel packet');
94 });
95
96 module('MP2T Parse Stream', {
97 setup: function() {
98 parseStream = new ParseStream();
99 }
100 });
101
102 test('emits an error on an invalid packet', function() {
103 var errors = [];
104 parseStream.on('error', function(error) {
105 errors.push(error);
106 });
107 parseStream.push(new Uint8Array(188));
108
109 equal(1, errors.length, 'emitted an error');
110 });
111
112 test('parses generic packet properties', function() {
113 var packet;
114 parseStream.on('data', function(data) {
115 packet = data;
116 });
117
118 parseStream.push(new Uint8Array([
119 0x47, // sync byte
120 // tei:0 pusi:1 tp:0 pid:0 0000 0000 0001 tsc:01 afc:10 cc:11 padding: 00
121 0x40, 0x01, 0x6c
122 ]));
123 ok(packet.payloadUnitStartIndicator, 'parsed payload_unit_start_indicator');
124 ok(packet.pid, 'parsed PID');
125 });
126
127 test('parses a data packet with adaptation fields', function() {
128 var packet;
129 parseStream.on('data', function(data) {
130 packet = data;
131 });
132
133 parseStream.push(new Uint8Array([
134 0x47, // sync byte
135 // 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
136 0x40, 0x00, 0x6c, 0x00, 0x00, 0x10
137 ]));
138 strictEqual(packet.type, 'pat', 'parsed the packet type');
139 });
140
141 test('parses a PES packet', function() {
142 var packet;
143 parseStream.on('data', function(data) {
144 packet = data;
145 });
146
147 // setup a program map table
148 parseStream.programMapTable = {
149 0x0010: videojs.mp2t.H264_STREAM_TYPE
150 };
151
152 parseStream.push(new Uint8Array([
153 0x47, // sync byte
154 // tei:0 pusi:1 tp:0 pid:0 0000 0000 0010 tsc:01 afc:01 cc:11 padding:00
155 0x40, 0x02, 0x5c
156 ]));
157 strictEqual(packet.type, 'pes', 'parsed a PES packet');
158 });
159
160 test('parses packets with variable length adaptation fields and a payload', function() {
161 var packet;
162 parseStream.on('data', function(data) {
163 packet = data;
164 });
165
166 // setup a program map table
167 parseStream.programMapTable = {
168 0x0010: videojs.mp2t.H264_STREAM_TYPE
169 };
170
171 parseStream.push(new Uint8Array([
172 0x47, // sync byte
173 // 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
174 0x40, 0x02, 0x7c, 0x0c, 0x00, 0x01
175 ]));
176 strictEqual(packet.type, 'pes', 'parsed a PES packet');
177 });
178
179 /*
180 Packet Header:
181 | sb | tei pusi tp pid:5 | pid | tsc afc cc |
182 with af:
183 | afl | ... | <data> |
184 without af:
185 | <data> |
186
187 PAT:
188 | pf? | ... |
189 | tid | ssi '0' r sl:4 | sl | tsi:8 |
190 | tsi | r vn cni | sn | lsn |
191
192 with program_number == '0':
193 | pn | pn | r np:5 | np |
194 otherwise:
195 | pn | pn | r pmp:5 | pmp |
196 */
197
198 test('parses the program map table pid from the program association table (PAT)', function() {
199 var packet;
200 parseStream.on('data', function(data) {
201 packet = data;
202 });
203
204 parseStream.push(new Uint8Array([
205 0x47, // sync byte
206 // tei:0 pusi:1 tp:0 pid:0 0000 0000 0000
207 0x40, 0x00,
208 // tsc:01 afc:01 cc:0000 pointer_field:0000 0000
209 0x50, 0x00,
210 // tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0000 0000
211 0x00, 0x00, 0x00,
212 // tsi:0000 0000 0000 0000
213 0x00, 0x00,
214 // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000
215 0x01, 0x00, 0x00,
216 // pn:0000 0000 0000 0001
217 0x00, 0x01,
218 // r:000 pmp:0 0000 0010 0000
219 0x00, 0x10,
220 // crc32:0000 0000 0000 0000 0000 0000 0000 0000
221 0x00, 0x00, 0x00, 0x00
222 ]));
223 strictEqual(0x0010, parseStream.pmtPid, 'parsed PMT pid');
224 });
225
226 test('parse the elementary streams from a program map table', function() {
227 var packet;
228 parseStream.on('data', function(data) {
229 packet = data;
230 });
231 parseStream.pmtPid = 0x0010;
232
233 parseStream.push(new Uint8Array([
234 0x47, // sync byte
235 // tei:0 pusi:1 tp:0 pid:0 0000 0010 0000
236 0x40, 0x10,
237 // tsc:01 afc:01 cc:0000 pointer_field:0000 0000
238 0x50, 0x00,
239 // tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0010 1111
240 0x00, 0x00, 0x2f,
241 // pn:0000 0000 0000 0001
242 0x00, 0x01,
243 // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000
244 0x01, 0x00, 0x00,
245 // r:000 ppid:0 0011 1111 1111
246 0x03, 0xff,
247 // r:0000 pil:0000 0000 0000
248 0x00, 0x00,
249 // h264
250 // st:0001 1010 r:000 epid:0 0000 0001 0001
251 0x1b, 0x00, 0x11,
252 // r:0000 esil:0000 0000 0000
253 0x00, 0x00,
254 // adts
255 // st:0000 1111 r:000 epid:0 0000 0001 0010
256 0x0f, 0x00, 0x12,
257 // r:0000 esil:0000 0000 0000
258 0x00, 0x00,
259 // crc
260 0x00, 0x00, 0x00, 0x00
261 ]));
262
263 ok(parseStream.programMapTable, 'parsed a program map');
264 strictEqual(0x1b, parseStream.programMapTable[0x11], 'associated h264 with pid 0x11');
265 strictEqual(0x0f, parseStream.programMapTable[0x12], 'associated adts with pid 0x12');
266 });
267
27 module('MP4 Transmuxer', { 268 module('MP4 Transmuxer', {
28 setup: function() { 269 setup: function() {
29 transmuxer = new Transmuxer(); 270 transmuxer = new Transmuxer();
......