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 @@ ...@@ -4,24 +4,30 @@
4 FlvTag = videojs.hls.FlvTag, 4 FlvTag = videojs.hls.FlvTag,
5 H264Stream = videojs.hls.H264Stream, 5 H264Stream = videojs.hls.H264Stream,
6 AacStream = videojs.hls.AacStream, 6 AacStream = videojs.hls.AacStream,
7 m2tsPacketSize = 188; 7 MP2T_PACKET_LENGTH,
8 8 STREAM_TYPES;
9 console.assert(H264Stream); 9
10 console.assert(AacStream); 10 /**
11 11 * An object that incrementally transmuxes MPEG2 Trasport Stream
12 window.videojs.hls.SegmentParser = function() { 12 * chunks into an FLV.
13 */
14 videojs.hls.SegmentParser = function() {
13 var 15 var
14 self = this, 16 self = this,
15 parseTSPacket, 17 parseTSPacket,
16 pmtPid, 18 streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH),
17 streamBuffer = new Uint8Array(m2tsPacketSize),
18 streamBufferByteCount = 0, 19 streamBufferByteCount = 0,
19 videoPid,
20 h264Stream = new H264Stream(), 20 h264Stream = new H264Stream(),
21 audioPid,
22 aacStream = new AacStream(), 21 aacStream = new AacStream(),
23 seekToKeyFrame = false; 22 seekToKeyFrame = false;
24 23
24 // expose the stream metadata
25 self.stream = {
26 // the mapping between transport stream programs and the PIDs
27 // that form their elementary streams
28 programMapTable: {}
29 };
30
25 // For information on the FLV format, see 31 // For information on the FLV format, see
26 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. 32 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
27 // Technically, this function returns the header and a metadata FLV tag 33 // Technically, this function returns the header and a metadata FLV tag
...@@ -146,24 +152,24 @@ ...@@ -146,24 +152,24 @@
146 // reconstruct the first packet. The rest of the packets will be 152 // reconstruct the first packet. The rest of the packets will be
147 // parsed directly from data 153 // parsed directly from data
148 if (streamBufferByteCount > 0) { 154 if (streamBufferByteCount > 0) {
149 if (data.byteLength + streamBufferByteCount < m2tsPacketSize) { 155 if (data.byteLength + streamBufferByteCount < MP2T_PACKET_LENGTH) {
150 // the current data is less than a single m2ts packet, so stash it 156 // the current data is less than a single m2ts packet, so stash it
151 // until we receive more 157 // until we receive more
152 158
153 // ?? this seems to append streamBuffer onto data and then just give up. I'm not sure why that would be interesting. 159 // ?? this seems to append streamBuffer onto data and then just give up. I'm not sure why that would be interesting.
154 videojs.log('data.length + streamBuffer.length < m2tsPacketSize ??'); 160 videojs.log('data.length + streamBuffer.length < MP2T_PACKET_LENGTH ??');
155 streamBuffer.readBytes(data, data.length, streamBuffer.length); 161 streamBuffer.readBytes(data, data.length, streamBuffer.length);
156 return; 162 return;
157 } else { 163 } else {
158 // we have enough data for an m2ts packet 164 // we have enough data for an m2ts packet
159 // process it immediately 165 // process it immediately
160 dataSlice = data.subarray(0, m2tsPacketSize - streamBufferByteCount); 166 dataSlice = data.subarray(0, MP2T_PACKET_LENGTH - streamBufferByteCount);
161 streamBuffer.set(dataSlice, streamBufferByteCount); 167 streamBuffer.set(dataSlice, streamBufferByteCount);
162 168
163 parseTSPacket(streamBuffer); 169 parseTSPacket(streamBuffer);
164 170
165 // reset the buffer 171 // reset the buffer
166 streamBuffer = new Uint8Array(m2tsPacketSize); 172 streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH);
167 streamBufferByteCount = 0; 173 streamBufferByteCount = 0;
168 } 174 }
169 } 175 }
...@@ -178,7 +184,7 @@ ...@@ -178,7 +184,7 @@
178 } 184 }
179 185
180 // base case: not enough data to parse a m2ts packet 186 // base case: not enough data to parse a m2ts packet
181 if (data.byteLength - dataPosition < m2tsPacketSize) { 187 if (data.byteLength - dataPosition < MP2T_PACKET_LENGTH) {
182 if (data.byteLength - dataPosition > 0) { 188 if (data.byteLength - dataPosition > 0) {
183 // there are bytes remaining, save them for next time 189 // there are bytes remaining, save them for next time
184 streamBuffer.set(data.subarray(dataPosition), 190 streamBuffer.set(data.subarray(dataPosition),
...@@ -189,8 +195,8 @@ ...@@ -189,8 +195,8 @@
189 } 195 }
190 196
191 // attempt to parse a m2ts packet 197 // attempt to parse a m2ts packet
192 if (parseTSPacket(data.subarray(dataPosition, dataPosition + m2tsPacketSize))) { 198 if (parseTSPacket(data.subarray(dataPosition, dataPosition + MP2T_PACKET_LENGTH))) {
193 dataPosition += m2tsPacketSize; 199 dataPosition += MP2T_PACKET_LENGTH;
194 } else { 200 } else {
195 // If there was an error parsing a TS packet. it could be 201 // If there was an error parsing a TS packet. it could be
196 // because we are not TS packet aligned. Step one forward by 202 // because we are not TS packet aligned. Step one forward by
...@@ -201,24 +207,31 @@ ...@@ -201,24 +207,31 @@
201 } 207 }
202 }; 208 };
203 209
210 /**
211 * Parses a video/mp2t packet and appends the underlying video and
212 * audio data onto h264stream and aacStream, respectively.
213 * @param data {Uint8Array} the bytes of an MPEG2-TS packet,
214 * including the sync byte.
215 * @return {boolean} whether a valid packet was encountered
216 */
204 // TODO add more testing to make sure we dont walk past the end of a TS 217 // TODO add more testing to make sure we dont walk past the end of a TS
205 // packet! 218 // packet!
206 parseTSPacket = function(data) { // :ByteArray):Boolean { 219 parseTSPacket = function(data) { // :ByteArray):Boolean {
207 var 220 var
208 offset = 0, // :uint 221 offset = 0, // :uint
209 end = offset + m2tsPacketSize, // :uint 222 end = offset + MP2T_PACKET_LENGTH, // :uint
210
211 // Don't look for a sync byte. We handle that in
212 // parseSegmentBinaryData()
213 223
214 // Payload Unit Start Indicator 224 // Payload Unit Start Indicator
215 pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000 225 pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000
216 226
217 // PacketId 227 // packet identifier (PID), a unique identifier for the elementary
228 // stream this packet describes
218 pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111 229 pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111
230
231 // adaptation_field_control, whether this header is followed by an
232 // adaptation field, a payload, or both
219 afflag = (data[offset + 3] & 0x30 ) >>> 4, 233 afflag = (data[offset + 3] & 0x30 ) >>> 4,
220 234
221 aflen, // :uint
222 patTableId, // :int 235 patTableId, // :int
223 patCurrentNextIndicator, // Boolean 236 patCurrentNextIndicator, // Boolean
224 patSectionLength, // :uint 237 patSectionLength, // :uint
...@@ -231,8 +244,8 @@ ...@@ -231,8 +244,8 @@
231 pts, // :uint 244 pts, // :uint
232 dts, // :uint 245 dts, // :uint
233 246
234 pmtTableId, // :int
235 pmtCurrentNextIndicator, // :Boolean 247 pmtCurrentNextIndicator, // :Boolean
248 pmtProgramDescriptorsLength,
236 pmtSectionLength, // :uint 249 pmtSectionLength, // :uint
237 250
238 streamType, // :int 251 streamType, // :int
...@@ -243,42 +256,64 @@ ...@@ -243,42 +256,64 @@
243 // corrupt stream detection 256 // corrupt stream detection
244 // cc = (data[offset + 3] & 0x0F); 257 // cc = (data[offset + 3] & 0x0F);
245 258
246 // Done with TS header 259 // move past the header
247 offset += 4; 260 offset += 4;
248 261
249 if (afflag > 0x01) { // skip most of the adaption field 262 // if an adaption field is present, its length is specified by
250 aflen = data[offset]; 263 // the fifth byte of the PES header. The adaptation field is
251 offset += aflen + 1; 264 // used to specify some forms of timing and control data that we
265 // do not currently use.
266 if (afflag > 0x01) {
267 offset += data[offset] + 1;
252 } 268 }
253 269
270 // Handle a Program Association Table (PAT). PATs map PIDs to
271 // individual programs. If this transport stream was being used
272 // for television broadcast, a program would probably be
273 // equivalent to a channel. In HLS, it would be very unusual to
274 // create an mp2t stream with multiple programs.
254 if (0x0000 === pid) { 275 if (0x0000 === pid) {
255 // always test for PMT first! (becuse other variables default to 0) 276 // The PAT may be split into multiple sections and those
256 277 // sections may be split into multiple packets. If a PAT
257 // if pusi is set we must skip X bytes (PSI pointer field) 278 // section starts in this packet, PUSI will be true and the
258 offset += pusi ? 1 + data[offset] : 0; 279 // first byte of the playload will indicate the offset from
280 // the current position to the start of the section.
281 if (pusi) {
282 offset += 1 + data[offset];
283 }
259 patTableId = data[offset]; 284 patTableId = data[offset];
260 285
261 console.assert(0x00 === patTableId, 'patTableId should be 0x00'); 286 if (patTableId !== 0x00) {
287 videojs.log('the table_id of the PAT should be 0x00 but was'
288 + patTableId.toString(16));
289 }
262 290
291 // the current_next_indicator specifies whether this PAT is
292 // currently applicable or is part of the next table to become
293 // active
263 patCurrentNextIndicator = !!(data[offset + 5] & 0x01); 294 patCurrentNextIndicator = !!(data[offset + 5] & 0x01);
264 if (patCurrentNextIndicator) { 295 if (patCurrentNextIndicator) {
296 // section_length specifies the number of bytes following
297 // its position to the end of this section
265 patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2]; 298 patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
266 offset += 8; // skip past PSI header 299 // move past the rest of the PSI header to the first program
267 300 // map table entry
268 // We currently only support streams with 1 program 301 offset += 8;
269 patSectionLength = (patSectionLength - 9) / 4; 302
270 if (1 !== patSectionLength) { 303 // we don't handle streams with more than one program, so
304 // raise an exception if we encounter one
305 // section_length = rest of header + (n * entry length) + CRC
306 // = 5 + (n * 4) + 4
307 if ((patSectionLength - 5 - 4) / 4 !== 1) {
271 throw new Error("TS has more that 1 program"); 308 throw new Error("TS has more that 1 program");
272 } 309 }
273 310
274 // if we ever support more that 1 program (unlikely) loop over them here 311 // the Program Map Table (PMT) associates the underlying
275 // var programNumber = data[offset + 0] << 8 | data[offset + 1]; 312 // video and audio streams with a unique PID
276 // var programId = (data[offset+2] & 0x1F) << 8 | data[offset + 3]; 313 self.stream.pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
277 pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
278 } 314 }
279 315 } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264] ||
280 // We could test the CRC here to detect corruption with extra CPU cost 316 pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
281 } else if (videoPid === pid || audioPid === pid) {
282 if (pusi) { 317 if (pusi) {
283 // comment out for speed 318 // comment out for speed
284 if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) { 319 if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) {
...@@ -322,60 +357,81 @@ ...@@ -322,60 +357,81 @@
322 // Skip past "optional" portion of PTS header 357 // Skip past "optional" portion of PTS header
323 offset += pesHeaderLength; 358 offset += pesHeaderLength;
324 359
325 if (videoPid === pid) { 360 if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
326 // Stash this frame for future use. 361 // Stash this frame for future use.
327 // console.assert(videoFrames.length < 3); 362 // console.assert(videoFrames.length < 3);
328 363
329 h264Stream.setNextTimeStamp(pts, 364 h264Stream.setNextTimeStamp(pts,
330 dts, 365 dts,
331 dataAlignmentIndicator); 366 dataAlignmentIndicator);
332 } else if (audioPid === pid) { 367 } else if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
333 aacStream.setNextTimeStamp(pts, 368 aacStream.setNextTimeStamp(pts,
334 pesPacketSize, 369 pesPacketSize,
335 dataAlignmentIndicator); 370 dataAlignmentIndicator);
336 } 371 }
337 } 372 }
338 373
339 if (audioPid === pid) { 374 if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
340 aacStream.writeBytes(data, offset, end - offset); 375 aacStream.writeBytes(data, offset, end - offset);
341 } else if (videoPid === pid) { 376 } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
342 h264Stream.writeBytes(data, offset, end - offset); 377 h264Stream.writeBytes(data, offset, end - offset);
343 } 378 }
344 } else if (pmtPid === pid) { 379 } else if (self.stream.pmtPid === pid) {
345 // TODO sanity check data[offset] 380 // similarly to the PAT, jump to the first byte of the section
346 // if pusi is set we must skip X bytes (PSI pointer field) 381 if (pusi) {
347 offset += (pusi ? 1 + data[offset] : 0); 382 offset += 1 + data[offset];
348 pmtTableId = data[offset]; 383 }
349 384 if (data[offset] !== 0x02) {
350 console.assert(0x02 === pmtTableId); 385 videojs.log('The table_id of a PMT should be 0x02 but was '
386 + data[offset].toString(16));
387 }
351 388
389 // whether this PMT is currently applicable or is part of the
390 // next table to become active
352 pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01); 391 pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01);
353 if (pmtCurrentNextIndicator) { 392 if (pmtCurrentNextIndicator) {
354 audioPid = videoPid = 0; 393 // overwrite any existing program map table
355 pmtSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2]; 394 self.stream.programMapTable = {};
395 // section_length specifies the number of bytes following
396 // its position to the end of this section
397 pmtSectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2];
398 // subtract the length of the program info descriptors
399 pmtProgramDescriptorsLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11];
400 pmtSectionLength -= pmtProgramDescriptorsLength;
356 // skip CRC and PSI data we dont care about 401 // skip CRC and PSI data we dont care about
402 // rest of header + CRC = 9 + 4
357 pmtSectionLength -= 13; 403 pmtSectionLength -= 13;
358 404
359 offset += 12; // skip past PSI header and some PMT data 405 // align offset to the first entry in the PMT
406 offset += 12 + pmtProgramDescriptorsLength;
407
408 // iterate through the entries
360 while (0 < pmtSectionLength) { 409 while (0 < pmtSectionLength) {
410 // the type of data carried in the PID this entry describes
361 streamType = data[offset + 0]; 411 streamType = data[offset + 0];
412 // the PID for this entry
362 elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2]; 413 elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
363 ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
364 offset += 5 + ESInfolength;
365 pmtSectionLength -= 5 + ESInfolength;
366 414
367 if (0x1B === streamType) { 415 if (streamType === STREAM_TYPES.h264) {
368 if (0 !== videoPid) { 416 if (self.stream.programMapTable[streamType] &&
417 self.stream.programMapTable[streamType] !== elementaryPID) {
369 throw new Error("Program has more than 1 video stream"); 418 throw new Error("Program has more than 1 video stream");
370 } 419 }
371 videoPid = elementaryPID; 420 self.stream.programMapTable[streamType] = elementaryPID;
372 } else if (0x0F === streamType) { 421 } else if (streamType === STREAM_TYPES.adts) {
373 if (0 !== audioPid) { 422 if (self.stream.programMapTable[streamType] &&
423 self.stream.programMapTable[streamType] !== elementaryPID) {
374 throw new Error("Program has more than 1 audio Stream"); 424 throw new Error("Program has more than 1 audio Stream");
375 } 425 }
376 audioPid = elementaryPID; 426 self.stream.programMapTable[streamType] = elementaryPID;
377 } 427 }
378 // TODO add support for MP3 audio 428 // TODO add support for MP3 audio
429
430 // the length of the entry descriptor
431 ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
432 // move to the first byte after the end of this entry
433 offset += 5 + ESInfolength;
434 pmtSectionLength -= 5 + ESInfolength;
379 } 435 }
380 } 436 }
381 // We could test the CRC here to detect corruption with extra CPU cost 437 // We could test the CRC here to detect corruption with extra CPU cost
...@@ -403,4 +459,12 @@ ...@@ -403,4 +459,12 @@
403 } 459 }
404 }; 460 };
405 }; 461 };
462
463 // MPEG2-TS constants
464 videojs.hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188;
465 videojs.hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = {
466 h264: 0x1b,
467 adts: 0x0f
468 };
469
406 })(window); 470 })(window);
......
...@@ -26,6 +26,9 @@ ...@@ -26,6 +26,9 @@
26 0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, 26 0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
27 0x09, 0x00, 0x00, 0x00, 0x00 27 0x09, 0x00, 0x00, 0x00, 0x00
28 ], 28 ],
29
30 extend = videojs.util.mergeOptions,
31
29 testAudioTag, 32 testAudioTag,
30 testVideoTag, 33 testVideoTag,
31 testScriptTag, 34 testScriptTag,
...@@ -51,6 +54,144 @@ ...@@ -51,6 +54,144 @@
51 deepEqual(expectedHeader, header, 'the rest of the header is correct'); 54 deepEqual(expectedHeader, header, 'the rest of the header is correct');
52 }); 55 });
53 56
57 test('parses PMTs with program descriptors', function() {
58 var
59 makePmt = function(options) {
60 var
61 result = [],
62 entryCount = 0,
63 k,
64 sectionLength;
65 for (k in options.pids) {
66 entryCount++;
67 }
68 // table_id
69 result.push(0x02);
70 // section_syntax_indicator '0' reserved section_length
71 // 13 + (program_info_length) + (n * 5 + ES_info_length[n])
72 sectionLength = 13 + (5 * entryCount) + 17;
73 result.push(0x80 | (0xF00 & sectionLength >>> 8));
74 result.push(sectionLength & 0xFF);
75 // program_number
76 result.push(0x00);
77 result.push(0x01);
78 // reserved version_number current_next_indicator
79 result.push(0x01);
80 // section_number
81 result.push(0x00);
82 // last_section_number
83 result.push(0x00);
84 // reserved PCR_PID
85 result.push(0xe1);
86 result.push(0x00);
87 // reserved program_info_length
88 result.push(0xf0);
89 result.push(0x11); // hard-coded 17 byte descriptor
90 // program descriptors
91 result = result.concat([
92 0x25, 0x0f, 0xff, 0xff,
93 0x49, 0x44, 0x33, 0x20,
94 0xff, 0x49, 0x44, 0x33,
95 0x20, 0x00, 0x1f, 0x00,
96 0x01
97 ]);
98 for (k in options.pids) {
99 // stream_type
100 result.push(options.pids[k]);
101 // reserved elementary_PID
102 result.push(0xe0 | (k & 0x1f00) >>> 8);
103 result.push(k & 0xff);
104 // reserved ES_info_length
105 result.push(0xf0);
106 result.push(0x00); // ES_info_length = 0
107 }
108 // CRC_32
109 result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
110 return result;
111 },
112 makePat = function(options) {
113 var
114 result = [],
115 k;
116 // table_id
117 result.push(0x00);
118 // section_syntax_indicator '0' reserved section_length
119 result.push(0x80);
120 result.push(0x0d); // section_length for one program
121 // transport_stream_id
122 result.push(0x00);
123 result.push(0x00);
124 // reserved version_number current_next_indicator
125 result.push(0x01); // current_next_indicator is 1
126 // section_number
127 result.push(0x00);
128 // last_section_number
129 result.push(0x00);
130 for (k in options.programs) {
131 // program_number
132 result.push((k & 0xFF00) >>> 8);
133 result.push(k & 0x00FF);
134 // reserved program_map_pid
135 result.push((options.programs[k] & 0x1f00) >>> 8);
136 result.push(options.programs[k] & 0xff);
137 }
138 return result;
139 },
140 makePsi = function(options) {
141 var result = [];
142
143 // pointer_field
144 if (options.payloadUnitStartIndicator) {
145 result.push(0x00);
146 }
147 if (options.programs) {
148 return result.concat(makePat(options));
149 }
150 return result.concat(makePmt(options));
151 },
152 makePacket = function(options) {
153 var
154 result = [],
155 settings = extend({
156 payloadUnitStartIndicator: true,
157 pid: 0x00
158 }, options),
159 pid;
160
161 // header
162 // sync_byte
163 result.push(0x47);
164 // transport_error_indicator payload_unit_start_indicator transport_priority PID
165 result.push((settings.pid & 0x1f) << 8 | 0x40);
166 result.push(settings.pid & 0xff);
167 // transport_scrambling_control adaptation_field_control continuity_counter
168 result.push(0x10);
169 result = result.concat(makePsi(settings));
170
171 // ensure the resulting packet is the correct size
172 result.length = videojs.hls.SegmentParser.MP2T_PACKET_LENGTH;
173 return result;
174 },
175 h264Type = videojs.hls.SegmentParser.STREAM_TYPES.h264,
176 adtsType = videojs.hls.SegmentParser.STREAM_TYPES.adts;
177
178 parser.parseSegmentBinaryData(new Uint8Array(makePacket({
179 programs: {
180 0x01: [0x01]
181 }
182 }).concat(makePacket({
183 pid: 0x01,
184 pids: {
185 0x02: h264Type, // h264 video
186 0x03: adtsType // adts audio
187 }
188 }))));
189
190 strictEqual(parser.stream.pmtPid, 0x01, 'PMT PID is 1');
191 strictEqual(parser.stream.programMapTable[h264Type], 0x02, 'video is PID 2');
192 strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3');
193 });
194
54 test('parses the first bipbop segment', function() { 195 test('parses the first bipbop segment', function() {
55 parser.parseSegmentBinaryData(window.bcSegment); 196 parser.parseSegmentBinaryData(window.bcSegment);
56 197
......