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.
Showing
2 changed files
with
273 additions
and
68 deletions
... | @@ -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 | ... | ... |
-
Please register or sign in to post a comment