bed2b294 by David LaPalomento

Parse program stream features out of transport streams

Add a program stream parser that aggregates program stream packets and re-emits them when the entire payload is available. Fix the test PES value to accurately use stuffing bytes. Update mp4 muxer page to exercise the TS parsing infrastructure.
1 parent 92b03b90
...@@ -14,9 +14,12 @@ ...@@ -14,9 +14,12 @@
14 (function(window, videojs, undefined) { 14 (function(window, videojs, undefined) {
15 'use strict'; 15 'use strict';
16 16
17 var PacketStream, ParseStream, MP2T_PACKET_LENGTH; 17 var PacketStream, ParseStream, ProgramStream, Transmuxer, MP2T_PACKET_LENGTH, H264_STREAM_TYPE, ADTS_STREAM_TYPE, mp4;
18 18
19 MP2T_PACKET_LENGTH = 188; // bytes 19 MP2T_PACKET_LENGTH = 188; // bytes
20 H264_STREAM_TYPE = 0x1b;
21 ADTS_STREAM_TYPE = 0x0f;
22 mp4 = videojs.mp4;
20 23
21 /** 24 /**
22 * Splits an incoming stream of binary data into MP2T packets. 25 * Splits an incoming stream of binary data into MP2T packets.
...@@ -78,7 +81,7 @@ PacketStream.prototype = new videojs.Hls.Stream(); ...@@ -78,7 +81,7 @@ PacketStream.prototype = new videojs.Hls.Stream();
78 */ 81 */
79 ParseStream = function() { 82 ParseStream = function() {
80 var parsePsi, parsePat, parsePmt, parsePes, self; 83 var parsePsi, parsePat, parsePmt, parsePes, self;
81 PacketStream.prototype.init.call(this); 84 ParseStream.prototype.init.call(this);
82 self = this; 85 self = this;
83 86
84 this.programMapTable = {}; 87 this.programMapTable = {};
...@@ -160,11 +163,12 @@ ParseStream = function() { ...@@ -160,11 +163,12 @@ ParseStream = function() {
160 }; 163 };
161 164
162 parsePes = function(payload, pes) { 165 parsePes = function(payload, pes) {
163 var ptsDtsFlags, 166 var ptsDtsFlags;
164 pesLength;
165 167
166 // PES packet length 168 if (!pes.payloadUnitStartIndicator) {
167 pesLength = payload[4] << 8 | payload[5]; 169 pes.data = payload;
170 return;
171 }
168 172
169 // find out if this packets starts a new keyframe 173 // find out if this packets starts a new keyframe
170 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; 174 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0;
...@@ -224,10 +228,11 @@ ParseStream = function() { ...@@ -224,10 +228,11 @@ ParseStream = function() {
224 result.pid <<= 8; 228 result.pid <<= 8;
225 result.pid |= packet[2]; 229 result.pid |= packet[2];
226 230
227 // if an adaption field is present, its length is specified by 231 // if an adaption field is present, its length is specified by the
228 // the fifth byte of the PES header. The adaptation field is 232 // fifth byte of the TS packet header. The adaptation field is
229 // used to specify some forms of timing and control data that we 233 // used to add stuffing to PES packets that don't fill a complete
230 // do not currently use. 234 // TS packet, and to specify some forms of timing and control data
235 // that we do not currently use.
231 if (((packet[3] & 0x30) >>> 4) > 0x01) { 236 if (((packet[3] & 0x30) >>> 4) > 0x01) {
232 offset += packet[offset] + 1; 237 offset += packet[offset] + 1;
233 } 238 }
...@@ -254,12 +259,129 @@ ParseStream.STREAM_TYPES = { ...@@ -254,12 +259,129 @@ ParseStream.STREAM_TYPES = {
254 adts: 0x0f 259 adts: 0x0f
255 }; 260 };
256 261
262 ProgramStream = function() {
263 var
264 // PES packet fragments
265 video = {
266 data: [],
267 size: 0
268 },
269 audio = {
270 data: [],
271 size: 0
272 },
273 flushStream = function(stream, type) {
274 var
275 event = {
276 type: type,
277 data: new Uint8Array(stream.size)
278 },
279 i = 0,
280 fragment;
281
282 // do nothing if there is no buffered data
283 if (!stream.data.length) {
284 return;
285 }
286
287 // reassemble the packet
288 while (stream.data.length) {
289 fragment = stream.data.shift();
290
291 event.data.set(fragment.data, i);
292 i += fragment.data.byteLength;
293 }
294 stream.size = 0;
295
296 self.trigger('data', event);
297 },
298 self;
299
300 ProgramStream.prototype.init.call(this);
301 self = this;
302
303 this.push = function(data) {
304 ({
305 pat: function() {
306 self.trigger('data', data);
307 },
308 pes: function() {
309 var stream, streamType;
310
311 if (data.streamType === H264_STREAM_TYPE) {
312 stream = video;
313 streamType = 'video';
314 } else {
315 stream = audio;
316 streamType = 'audio';
317 }
318
319 // if a new packet is starting, we can flush the completed
320 // packet
321 if (data.payloadUnitStartIndicator) {
322 flushStream(stream, streamType);
323 }
324
325 // buffer this fragment until we are sure we've received the
326 // complete payload
327 stream.data.push(data);
328 stream.size += data.data.byteLength;
329 },
330 pmt: function() {
331 self.trigger('data', data);
332 }
333 })[data.type]();
334 };
335
336 /**
337 * Flush any remaining input. Video PES packets may be of variable
338 * length. Normally, the start of a new video packet can trigger the
339 * finalization of the previous packet. That is not possible if no
340 * more video is forthcoming, however. In that case, some other
341 * mechanism (like the end of the file) has to be employed. When it is
342 * clear that no additional data is forthcoming, calling this method
343 * will flush the buffered packets.
344 */
345 this.end = function() {
346 flushStream(video, 'video');
347 flushStream(audio, 'audio');
348 };
349 };
350 ProgramStream.prototype = new videojs.Hls.Stream();
351
352 Transmuxer = function() {
353 var self = this, packetStream, parseStream, programStream;
354 Transmuxer.prototype.init.call(this);
355
356 // set up the parsing pipeline
357 packetStream = new PacketStream();
358 parseStream = new ParseStream();
359 programStream = new ProgramStream();
360
361 packetStream.pipe(parseStream);
362 parseStream.pipe(programStream);
363
364 programStream.on('data', function(data) {
365 self.trigger('data', data);
366 });
367
368 // feed incoming data to the front of the parsing pipeline
369 this.push = function(data) {
370 packetStream.push(data);
371 };
372 // flush any buffered data
373 this.end = programStream.end;
374 };
375 Transmuxer.prototype = new videojs.Hls.Stream();
376
257 window.videojs.mp2t = { 377 window.videojs.mp2t = {
258 PAT_PID: 0x0000, 378 PAT_PID: 0x0000,
259 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH, 379 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH,
260 H264_STREAM_TYPE: 0x1b, 380 H264_STREAM_TYPE: H264_STREAM_TYPE,
261 ADTS_STREAM_TYPE: 0x0f, 381 ADTS_STREAM_TYPE: ADTS_STREAM_TYPE,
262 PacketStream: PacketStream, 382 PacketStream: PacketStream,
263 ParseStream: ParseStream 383 ParseStream: ParseStream,
384 ProgramStream: ProgramStream,
385 Transmuxer: Transmuxer
264 }; 386 };
265 })(window, window.videojs); 387 })(window, window.videojs);
......
...@@ -24,7 +24,18 @@ var ...@@ -24,7 +24,18 @@ var
24 PacketStream = videojs.mp2t.PacketStream, 24 PacketStream = videojs.mp2t.PacketStream,
25 packetStream, 25 packetStream,
26 ParseStream = videojs.mp2t.ParseStream, 26 ParseStream = videojs.mp2t.ParseStream,
27 parseStream; 27 parseStream,
28 ProgramStream = videojs.mp2t.ProgramStream,
29 programStream,
30
31 MP2T_PACKET_LENGTH = videojs.mp2t.MP2T_PACKET_LENGTH,
32 H264_STREAM_TYPE = videojs.mp2t.H264_STREAM_TYPE,
33 ADTS_STREAM_TYPE = videojs.mp2t.ADTS_STREAM_TYPE,
34 packetize,
35
36 PAT,
37 PMT,
38 standalonePes;
28 39
29 module('MP2T Packet Stream', { 40 module('MP2T Packet Stream', {
30 setup: function() { 41 setup: function() {
...@@ -91,9 +102,12 @@ test('buffers extra after multiple packets', function() { ...@@ -91,9 +102,12 @@ test('buffers extra after multiple packets', function() {
91 equal(188, datas[2].length, 'parsed the finel packet'); 102 equal(188, datas[2].length, 'parsed the finel packet');
92 }); 103 });
93 104
94 module('MP2T Parse Stream', { 105 module('MP2T ParseStream', {
95 setup: function() { 106 setup: function() {
107 packetStream = new PacketStream();
96 parseStream = new ParseStream(); 108 parseStream = new ParseStream();
109
110 packetStream.pipe(parseStream);
97 } 111 }
98 }); 112 });
99 113
...@@ -122,6 +136,21 @@ test('parses generic packet properties', function() { ...@@ -122,6 +136,21 @@ test('parses generic packet properties', function() {
122 ok(packet.pid, 'parsed PID'); 136 ok(packet.pid, 'parsed PID');
123 }); 137 });
124 138
139 test('parses piped data events', function() {
140 var packet;
141 parseStream.on('data', function(data) {
142 packet = data;
143 });
144
145 parseStream.push(new Uint8Array([
146 0x47, // sync byte
147 // tei:0 pusi:1 tp:0 pid:0 0000 0000 0001 tsc:01 afc:10 cc:11 padding: 00
148 0x40, 0x01, 0x6c
149 ]));
150
151 ok(packet, 'parsed a packet');
152 });
153
125 test('parses a data packet with adaptation fields', function() { 154 test('parses a data packet with adaptation fields', function() {
126 var packet; 155 var packet;
127 parseStream.on('data', function(data) { 156 parseStream.on('data', function(data) {
...@@ -193,13 +222,7 @@ otherwise: ...@@ -193,13 +222,7 @@ otherwise:
193 | pn | pn | r pmp:5 | pmp | 222 | pn | pn | r pmp:5 | pmp |
194 */ 223 */
195 224
196 test('parses the program map table pid from the program association table (PAT)', function() { 225 PAT = [
197 var packet;
198 parseStream.on('data', function(data) {
199 packet = data;
200 });
201
202 parseStream.push(new Uint8Array([
203 0x47, // sync byte 226 0x47, // sync byte
204 // tei:0 pusi:1 tp:0 pid:0 0000 0000 0000 227 // tei:0 pusi:1 tp:0 pid:0 0000 0000 0000
205 0x40, 0x00, 228 0x40, 0x00,
...@@ -217,19 +240,20 @@ test('parses the program map table pid from the program association table (PAT)' ...@@ -217,19 +240,20 @@ test('parses the program map table pid from the program association table (PAT)'
217 0x00, 0x10, 240 0x00, 0x10,
218 // crc32:0000 0000 0000 0000 0000 0000 0000 0000 241 // crc32:0000 0000 0000 0000 0000 0000 0000 0000
219 0x00, 0x00, 0x00, 0x00 242 0x00, 0x00, 0x00, 0x00
220 ])); 243 ];
221 ok(packet, 'parsed a packet');
222 strictEqual(0x0010, parseStream.pmtPid, 'parsed PMT pid');
223 });
224 244
225 test('parse the elementary streams from a program map table', function() { 245 test('parses the program map table pid from the program association table (PAT)', function() {
226 var packet; 246 var packet;
227 parseStream.on('data', function(data) { 247 parseStream.on('data', function(data) {
228 packet = data; 248 packet = data;
229 }); 249 });
230 parseStream.pmtPid = 0x0010;
231 250
232 parseStream.push(new Uint8Array([ 251 parseStream.push(new Uint8Array(PAT));
252 ok(packet, 'parsed a packet');
253 strictEqual(0x0010, parseStream.pmtPid, 'parsed PMT pid');
254 });
255
256 PMT = [
233 0x47, // sync byte 257 0x47, // sync byte
234 // tei:0 pusi:1 tp:0 pid:0 0000 0010 0000 258 // tei:0 pusi:1 tp:0 pid:0 0000 0010 0000
235 0x40, 0x10, 259 0x40, 0x10,
...@@ -257,7 +281,16 @@ test('parse the elementary streams from a program map table', function() { ...@@ -257,7 +281,16 @@ test('parse the elementary streams from a program map table', function() {
257 0x00, 0x00, 281 0x00, 0x00,
258 // crc 282 // crc
259 0x00, 0x00, 0x00, 0x00 283 0x00, 0x00, 0x00, 0x00
260 ])); 284 ];
285
286 test('parse the elementary streams from a program map table', function() {
287 var packet;
288 parseStream.on('data', function(data) {
289 packet = data;
290 });
291 parseStream.pmtPid = 0x0010;
292
293 parseStream.push(new Uint8Array(PMT));
261 294
262 ok(packet, 'parsed a packet'); 295 ok(packet, 'parsed a packet');
263 ok(parseStream.programMapTable, 'parsed a program map'); 296 ok(parseStream.programMapTable, 'parsed a program map');
...@@ -351,23 +384,44 @@ test('parses an elementary stream packet with a pts and dts', function() { ...@@ -351,23 +384,44 @@ test('parses an elementary stream packet with a pts and dts', function() {
351 equal(2 / 90, packet.dts, 'parsed the dts'); 384 equal(2 / 90, packet.dts, 'parsed the dts');
352 }); 385 });
353 386
354 test('parses an elementary stream packet without a pts or dts', function() { 387 standalonePes = [
355
356 var packet;
357 parseStream.on('data', function(data) {
358 packet = data;
359 });
360
361 parseStream.programMapTable = {
362 0x11: 0x1b // pid 0x11 is h264 data
363 };
364
365 parseStream.push(new Uint8Array([
366 0x47, // sync byte 388 0x47, // sync byte
367 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001 389 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001
368 0x40, 0x11, 390 0x40, 0x11,
369 // tsc:01 afc:01 cc:0000 391 // tsc:01 afc:11 cc:0000
370 0x50, 392 0x70,
393 // afl:1010 1100
394 0xac,
395 // di:0 rai:0 espi:0 pf:0 of:0 spf:0 tpdf:0 afef:0
396 0x00,
397 // stuffing_bytes (171 bytes)
398 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
399 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
400 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
401 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
402
403 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
404 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
405 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
406 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
407
408 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
409 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
410 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
411 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
412
413 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
414 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
415 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
416 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
417
418 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
419 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
420 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
421 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
422
423 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
424 0xff, 0xff, 0xff,
371 // pscp:0000 0000 0000 0000 0000 0001 425 // pscp:0000 0000 0000 0000 0000 0001
372 0x00, 0x00, 0x01, 426 0x00, 0x00, 0x01,
373 // sid:0000 0000 ppl:0000 0000 0000 0101 427 // sid:0000 0000 ppl:0000 0000 0000 0101
...@@ -380,7 +434,21 @@ test('parses an elementary stream packet without a pts or dts', function() { ...@@ -380,7 +434,21 @@ test('parses an elementary stream packet without a pts or dts', function() {
380 0x00, 434 0x00,
381 // "data":1010 1111 0000 0001 435 // "data":1010 1111 0000 0001
382 0xaf, 0x01 436 0xaf, 0x01
383 ])); 437 ];
438
439 test('parses an elementary stream packet without a pts or dts', function() {
440
441 var packet;
442 parseStream.on('data', function(data) {
443 packet = data;
444 });
445
446 // pid 0x11 is h264 data
447 parseStream.programMapTable = {
448 0x11: H264_STREAM_TYPE
449 };
450
451 parseStream.push(new Uint8Array(standalonePes));
384 452
385 ok(packet, 'parsed a packet'); 453 ok(packet, 'parsed a packet');
386 equal('pes', packet.type, 'recognized a PES packet'); 454 equal('pes', packet.type, 'recognized a PES packet');
...@@ -392,4 +460,153 @@ test('parses an elementary stream packet without a pts or dts', function() { ...@@ -392,4 +460,153 @@ test('parses an elementary stream packet without a pts or dts', function() {
392 ok(!packet.dts, 'did not parse a dts'); 460 ok(!packet.dts, 'did not parse a dts');
393 }); 461 });
394 462
463 module('MP2T ProgramStream', {
464 setup: function() {
465 programStream = new ProgramStream();
466 }
467 });
468
469 packetize = function(data) {
470 var packet = new Uint8Array(MP2T_PACKET_LENGTH);
471 packet.set(data);
472 return packet;
473 };
474
475 test('parses metadata events from PSI packets', function() {
476 var metadatas = 0, datas = 0;
477 programStream.on('data', function(data) {
478 if (data.type === 'pat' || data.type === 'pmt') {
479 metadatas++;
480 }
481 datas++;
482 });
483 programStream.push({
484 type: 'pat'
485 });
486 programStream.push({
487 type: 'pmt'
488 });
489
490 equal(2, datas, 'data fired');
491 equal(2, metadatas, 'metadata generated');
492 });
493
494 test('parses standalone program stream packets', function() {
495 var packets = [];
496 programStream.on('data', function(packet) {
497 packets.push(packet);
498 });
499 programStream.push({
500 type: 'pes',
501 data: new Uint8Array(19)
502 });
503 programStream.end();
504
505 equal(1, packets.length, 'built one packet');
506 equal('audio', packets[0].type, 'identified audio data');
507 equal(19, packets[0].data.byteLength, 'parsed the correct payload size');
508 });
509
510 test('aggregates program stream packets from the transport stream', function() {
511 var events = [];
512 programStream.on('data', function(event) {
513 events.push(event);
514 });
515
516 programStream.push({
517 type: 'pes',
518 streamType: H264_STREAM_TYPE,
519 data: new Uint8Array(7)
520 });
521 equal(0, events.length, 'buffers partial packets');
522
523 programStream.push({
524 type: 'pes',
525 streamType: H264_STREAM_TYPE,
526 data: new Uint8Array(13)
527 });
528 programStream.end();
529 equal(1, events.length, 'built one packet');
530 equal('video', events[0].type, 'identified video data');
531 equal(20, events[0].data.byteLength, 'concatenated transport packets');
532 });
533
534 test('buffers audio and video program streams individually', function() {
535 var events = [];
536 programStream.on('data', function(event) {
537 events.push(event);
538 });
539
540 programStream.push({
541 type: 'pes',
542 payloadUnitStartIndicator: true,
543 streamType: H264_STREAM_TYPE,
544 data: new Uint8Array(1)
545 });
546 programStream.push({
547 type: 'pes',
548 payloadUnitStartIndicator: true,
549 streamType: ADTS_STREAM_TYPE,
550 data: new Uint8Array(1)
551 });
552 equal(0, events.length, 'buffers partial packets');
553
554 programStream.push({
555 type: 'pes',
556 streamType: H264_STREAM_TYPE,
557 data: new Uint8Array(1)
558 });
559 programStream.push({
560 type: 'pes',
561 streamType: ADTS_STREAM_TYPE,
562 data: new Uint8Array(1)
563 });
564 programStream.end();
565 equal(2, events.length, 'parsed a complete packet');
566 equal('video', events[0].type, 'identified video data');
567 equal('audio', events[1].type, 'identified audio data');
568 });
569
570 test('flushes the buffered packets when a new one of that type is started', function() {
571 var packets = [];
572 programStream.on('data', function(packet) {
573 packets.push(packet);
574 });
575 programStream.push({
576 type: 'pes',
577 payloadUnitStartIndicator: true,
578 streamType: H264_STREAM_TYPE,
579 data: new Uint8Array(1)
580 });
581 programStream.push({
582 type: 'pes',
583 payloadUnitStartIndicator: true,
584 streamType: ADTS_STREAM_TYPE,
585 data: new Uint8Array(7)
586 });
587 programStream.push({
588 type: 'pes',
589 streamType: H264_STREAM_TYPE,
590 data: new Uint8Array(1)
591 });
592 equal(0, packets.length, 'buffers packets by type');
593
594 programStream.push({
595 type: 'pes',
596 payloadUnitStartIndicator: true,
597 streamType: H264_STREAM_TYPE,
598 data: new Uint8Array(1)
599 });
600 equal(1, packets.length, 'built one packet');
601 equal('video', packets[0].type, 'identified video data');
602 equal(2, packets[0].data.byteLength, 'concatenated packets');
603
604 programStream.end();
605 equal(3, packets.length, 'built tow more packets');
606 equal('video', packets[1].type, 'identified video data');
607 equal(1, packets[1].data.byteLength, 'parsed the video payload');
608 equal('audio', packets[2].type, 'identified audio data');
609 equal(7, packets[2].data.byteLength, 'parsed the audio payload');
610 });
611
395 })(window, window.videojs); 612 })(window, window.videojs);
......