Generate init segment based on elementary streams
Capture the number of elementary streams in a transport stream program and map those to mp4 tracks in the init segment. Fix up some examples and update MSE documentation.
Showing
5 changed files
with
326 additions
and
127 deletions
... | @@ -9,8 +9,18 @@ Chrome 36. | ... | @@ -9,8 +9,18 @@ Chrome 36. |
9 | ## ISO Base Media File Format (BMFF) | 9 | ## ISO Base Media File Format (BMFF) |
10 | 10 | ||
11 | ### Init Segment | 11 | ### Init Segment |
12 | A working initialization segment is outlined below. It may be possible | ||
13 | to trim this structure down further. | ||
14 | |||
12 | - `ftyp` | 15 | - `ftyp` |
13 | - `moov` | 16 | - `moov` |
17 | - `mvhd` | ||
18 | - `trak` | ||
19 | - `tkhd` | ||
20 | - `mdia` | ||
21 | - `mdhd` | ||
22 | - `hdlr` | ||
23 | - `minf` | ||
14 | - `mvex` | 24 | - `mvex` |
15 | 25 | ||
16 | ### Media Segment | 26 | ### Media Segment |
... | @@ -22,7 +32,7 @@ movie data is outlined below: | ... | @@ -22,7 +32,7 @@ movie data is outlined below: |
22 | - `traf` | 32 | - `traf` |
23 | - `tfhd` | 33 | - `tfhd` |
24 | - `tfdt` | 34 | - `tfdt` |
25 | - `trun` | 35 | - `trun` containing samples |
26 | - `mdat` | 36 | - `mdat` |
27 | 37 | ||
28 | ### Structure | 38 | ### Structure | ... | ... |
... | @@ -221,8 +221,19 @@ moof = function(sequenceNumber, tracks) { | ... | @@ -221,8 +221,19 @@ moof = function(sequenceNumber, tracks) { |
221 | mfhd(sequenceNumber), | 221 | mfhd(sequenceNumber), |
222 | box.apply(null, trafCall)); | 222 | box.apply(null, trafCall)); |
223 | }; | 223 | }; |
224 | moov = function(duration, width, height, type) { | 224 | /** |
225 | return box(types.moov, mvhd(duration), trak(duration, width, height, type), mvex()); | 225 | * @param tracks... (optional) {array} the tracks associated with this movie |
226 | */ | ||
227 | moov = function(tracks) { | ||
228 | var | ||
229 | i = tracks.length, | ||
230 | boxes = []; | ||
231 | |||
232 | while (i--) { | ||
233 | boxes[i] = trak(tracks[i]); | ||
234 | } | ||
235 | |||
236 | return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex())); | ||
226 | }; | 237 | }; |
227 | mvex = function() { | 238 | mvex = function() { |
228 | return box(types.mvex, box(types.trex, TREX)); | 239 | return box(types.mvex, box(types.trex, TREX)); |
... | @@ -331,18 +342,21 @@ styp = function() { | ... | @@ -331,18 +342,21 @@ styp = function() { |
331 | return box(types.styp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND); | 342 | return box(types.styp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND); |
332 | }; | 343 | }; |
333 | 344 | ||
334 | tkhd = function(duration, width, height) { | 345 | tkhd = function(track) { |
335 | return box(types.tkhd, new Uint8Array([ | 346 | return box(types.tkhd, new Uint8Array([ |
336 | 0x00, // version 0 | 347 | 0x00, // version 0 |
337 | 0x00, 0x00, 0x00, // flags | 348 | 0x00, 0x00, 0x00, // flags |
338 | 0x00, 0x00, 0x00, 0x00, // creation_time | 349 | 0x00, 0x00, 0x00, 0x00, // creation_time |
339 | 0x00, 0x00, 0x00, 0x00, // modification_time | 350 | 0x00, 0x00, 0x00, 0x00, // modification_time |
340 | 0x00, 0x00, 0x00, 0x01, // track_ID | 351 | (track.id & 0xFF000000) >> 24, |
352 | (track.id & 0xFF0000) >> 16, | ||
353 | (track.id & 0xFF00) >> 8, | ||
354 | track.id & 0xFF, // track_ID | ||
341 | 0x00, 0x00, 0x00, 0x00, // reserved | 355 | 0x00, 0x00, 0x00, 0x00, // reserved |
342 | (duration & 0xFF000000) >> 24, | 356 | (track.duration & 0xFF000000) >> 24, |
343 | (duration & 0xFF0000) >> 16, | 357 | (track.duration & 0xFF0000) >> 16, |
344 | (duration & 0xFF00) >> 8, | 358 | (track.duration & 0xFF00) >> 8, |
345 | duration & 0xFF, // duration | 359 | track.duration & 0xFF, // duration |
346 | 0x00, 0x00, 0x00, 0x00, | 360 | 0x00, 0x00, 0x00, 0x00, |
347 | 0x00, 0x00, 0x00, 0x00, // reserved | 361 | 0x00, 0x00, 0x00, 0x00, // reserved |
348 | 0x00, 0x00, // layer | 362 | 0x00, 0x00, // layer |
... | @@ -358,17 +372,25 @@ tkhd = function(duration, width, height) { | ... | @@ -358,17 +372,25 @@ tkhd = function(duration, width, height) { |
358 | 0x00, 0x00, 0x00, 0x00, | 372 | 0x00, 0x00, 0x00, 0x00, |
359 | 0x00, 0x00, 0x00, 0x00, | 373 | 0x00, 0x00, 0x00, 0x00, |
360 | 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix | 374 | 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix |
361 | (width & 0xFF00) >> 8, | 375 | (track.width & 0xFF00) >> 8, |
362 | width & 0xFF, | 376 | track.width & 0xFF, |
363 | 0x00, 0x00, // width | 377 | 0x00, 0x00, // width |
364 | (height & 0xFF00) >> 8, | 378 | (track.height & 0xFF00) >> 8, |
365 | height & 0xFF, | 379 | track.height & 0xFF, |
366 | 0x00, 0x00 // height | 380 | 0x00, 0x00 // height |
367 | ])); | 381 | ])); |
368 | }; | 382 | }; |
369 | 383 | ||
370 | trak = function(duration, width, height, type) { | 384 | /** |
371 | return box(types.trak, tkhd(duration, width, height), mdia(duration, width, height, type)); | 385 | * Generate a track box. |
386 | * @param track {object} a track definition | ||
387 | * @return {Uint8Array} the track box | ||
388 | */ | ||
389 | trak = function(track) { | ||
390 | track.duration = track.duration || 0xffffffff; | ||
391 | return box(types.trak, | ||
392 | tkhd(track), | ||
393 | mdia(track.duration, track.width, track.height, track.type)); | ||
372 | }; | 394 | }; |
373 | 395 | ||
374 | window.videojs.mp4 = { | 396 | window.videojs.mp4 = { |
... | @@ -376,12 +398,13 @@ window.videojs.mp4 = { | ... | @@ -376,12 +398,13 @@ window.videojs.mp4 = { |
376 | mdat: mdat, | 398 | mdat: mdat, |
377 | moof: moof, | 399 | moof: moof, |
378 | moov: moov, | 400 | moov: moov, |
379 | initSegment: function() { | 401 | initSegment: function(tracks) { |
380 | var | 402 | var |
381 | fileType = ftyp(), | 403 | fileType = ftyp(), |
382 | movie = moov(0xffffffff, 1280, 720, "video"), | 404 | movie = moov(tracks), |
383 | result = new Uint8Array(fileType.byteLength + movie.byteLength); | 405 | result; |
384 | 406 | ||
407 | result = new Uint8Array(fileType.byteLength + movie.byteLength); | ||
385 | result.set(fileType); | 408 | result.set(fileType); |
386 | result.set(movie, fileType.byteLength); | 409 | result.set(movie, fileType.byteLength); |
387 | return result; | 410 | return result; | ... | ... |
... | @@ -124,10 +124,7 @@ ParseStream = function() { | ... | @@ -124,10 +124,7 @@ ParseStream = function() { |
124 | * fields parsed from the PMT. | 124 | * fields parsed from the PMT. |
125 | */ | 125 | */ |
126 | parsePmt = function(payload, pmt) { | 126 | parsePmt = function(payload, pmt) { |
127 | var tableEnd, programInfoLength, offset; | 127 | var sectionLength, tableEnd, programInfoLength, offset; |
128 | |||
129 | pmt.section_number = payload[6]; | ||
130 | pmt.last_section_number = payload[7]; | ||
131 | 128 | ||
132 | // PMTs can be sent ahead of the time when they should actually | 129 | // PMTs can be sent ahead of the time when they should actually |
133 | // take effect. We don't believe this should ever be the case | 130 | // take effect. We don't believe this should ever be the case |
... | @@ -141,9 +138,11 @@ ParseStream = function() { | ... | @@ -141,9 +138,11 @@ ParseStream = function() { |
141 | // overwrite any existing program map table | 138 | // overwrite any existing program map table |
142 | self.programMapTable = {}; | 139 | self.programMapTable = {}; |
143 | 140 | ||
144 | // the mapping table ends right before the 32-bit CRC | 141 | // the mapping table ends at the end of the current section |
145 | tableEnd = payload.byteLength - 4; | 142 | sectionLength = (payload[1] & 0x0f) << 8 | payload[2]; |
146 | // to determine where the table starts, we have to figure out how | 143 | tableEnd = 3 + sectionLength - 4; |
144 | |||
145 | // to determine where the table is, we have to figure out how | ||
147 | // long the program info descriptors are | 146 | // long the program info descriptors are |
148 | programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; | 147 | programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; |
149 | 148 | ||
... | @@ -273,7 +272,7 @@ ProgramStream = function() { | ... | @@ -273,7 +272,7 @@ ProgramStream = function() { |
273 | data: [], | 272 | data: [], |
274 | size: 0 | 273 | size: 0 |
275 | }, | 274 | }, |
276 | flushStream = function(stream, type, pes) { | 275 | flushStream = function(stream, type) { |
277 | var | 276 | var |
278 | event = { | 277 | event = { |
279 | type: type, | 278 | type: type, |
... | @@ -282,16 +281,6 @@ ProgramStream = function() { | ... | @@ -282,16 +281,6 @@ ProgramStream = function() { |
282 | i = 0, | 281 | i = 0, |
283 | fragment; | 282 | fragment; |
284 | 283 | ||
285 | if ( pes !== undefined) { | ||
286 | // move over data from PES into Stream frame | ||
287 | event.pes = {}; | ||
288 | event.pes.pts = pes.pts; | ||
289 | event.pes.dts = pes.dts; | ||
290 | event.pes.pid = pes.pid; | ||
291 | event.pes.dataAlignmentIndicator = pes.dataAlignmentIndicator; | ||
292 | event.pes.payloadUnitStartIndicator = pes.payloadUnitStartIndicator; | ||
293 | } | ||
294 | |||
295 | // do nothing if there is no buffered data | 284 | // do nothing if there is no buffered data |
296 | if (!stream.data.length) { | 285 | if (!stream.data.length) { |
297 | return; | 286 | return; |
... | @@ -333,7 +322,7 @@ ProgramStream = function() { | ... | @@ -333,7 +322,7 @@ ProgramStream = function() { |
333 | // if a new packet is starting, we can flush the completed | 322 | // if a new packet is starting, we can flush the completed |
334 | // packet | 323 | // packet |
335 | if (data.payloadUnitStartIndicator) { | 324 | if (data.payloadUnitStartIndicator) { |
336 | flushStream(stream, streamType, data); | 325 | flushStream(stream, streamType); |
337 | } | 326 | } |
338 | 327 | ||
339 | // buffer this fragment until we are sure we've received the | 328 | // buffer this fragment until we are sure we've received the |
... | @@ -358,8 +347,10 @@ ProgramStream = function() { | ... | @@ -358,8 +347,10 @@ ProgramStream = function() { |
358 | track.id = +k; | 347 | track.id = +k; |
359 | if (programMapTable[k] === H264_STREAM_TYPE) { | 348 | if (programMapTable[k] === H264_STREAM_TYPE) { |
360 | track.codec = 'avc'; | 349 | track.codec = 'avc'; |
350 | track.type = 'video'; | ||
361 | } else if (programMapTable[k] === ADTS_STREAM_TYPE) { | 351 | } else if (programMapTable[k] === ADTS_STREAM_TYPE) { |
362 | track.codec = 'adts'; | 352 | track.codec = 'adts'; |
353 | track.type = 'audio'; | ||
363 | } | 354 | } |
364 | event.tracks.push(track); | 355 | event.tracks.push(track); |
365 | } | 356 | } |
... | @@ -412,9 +403,18 @@ H264Stream = function() { | ... | @@ -412,9 +403,18 @@ H264Stream = function() { |
412 | self = this; | 403 | self = this; |
413 | 404 | ||
414 | this.push = function(packet) { | 405 | this.push = function(packet) { |
415 | if (packet.type === 'video') { | 406 | if (packet.type !== 'video') { |
416 | this.trigger('data', packet); | 407 | return; |
408 | } | ||
409 | switch (packet.data[0]) { | ||
410 | case 0x09: | ||
411 | packet.nalUnitType = 'access_unit_delimiter_rbsp'; | ||
412 | break; | ||
413 | |||
414 | default: | ||
415 | break; | ||
417 | } | 416 | } |
417 | this.trigger('data', packet); | ||
418 | }; | 418 | }; |
419 | }; | 419 | }; |
420 | H264Stream.prototype = new videojs.Hls.Stream(); | 420 | H264Stream.prototype = new videojs.Hls.Stream(); |
... | @@ -424,7 +424,14 @@ Transmuxer = function() { | ... | @@ -424,7 +424,14 @@ Transmuxer = function() { |
424 | var | 424 | var |
425 | self = this, | 425 | self = this, |
426 | sequenceNumber = 0, | 426 | sequenceNumber = 0, |
427 | packetStream, parseStream, programStream, aacStream, h264Stream; | 427 | initialized = false, |
428 | videoSamples = [], | ||
429 | videoSamplesSize = 0, | ||
430 | |||
431 | packetStream, parseStream, programStream, aacStream, h264Stream, | ||
432 | |||
433 | flushVideo; | ||
434 | |||
428 | Transmuxer.prototype.init.call(this); | 435 | Transmuxer.prototype.init.call(this); |
429 | 436 | ||
430 | // set up the parsing pipeline | 437 | // set up the parsing pipeline |
... | @@ -439,16 +446,26 @@ Transmuxer = function() { | ... | @@ -439,16 +446,26 @@ Transmuxer = function() { |
439 | programStream.pipe(aacStream); | 446 | programStream.pipe(aacStream); |
440 | programStream.pipe(h264Stream); | 447 | programStream.pipe(h264Stream); |
441 | 448 | ||
442 | // generate an init segment | 449 | // helper functions |
443 | this.initSegment = mp4.initSegment(); | 450 | flushVideo = function() { |
451 | var moof, mdat, boxes, i, data; | ||
444 | 452 | ||
445 | h264Stream.on('data', function(data) { | 453 | moof = mp4.moof(sequenceNumber, []); |
446 | var | 454 | |
447 | moof = mp4.moof(sequenceNumber, []), | 455 | // concatenate the video data and construct the mdat |
448 | mdat = mp4.mdat(data.data), | 456 | data = new Uint8Array(videoSamplesSize); |
449 | // it would be great to allocate this array up front instead of | 457 | i = 0; |
450 | // throwing away hundreds of media segment fragments | 458 | while (videoSamples.length) { |
451 | boxes = new Uint8Array(moof.byteLength + mdat.byteLength); | 459 | data.set(videoSamples[0].data, i); |
460 | i += videoSamples[0].data.byteLength; | ||
461 | videoSamples.shift(); | ||
462 | } | ||
463 | videoSamplesSize = 0; | ||
464 | mdat = mp4.mdat(data); | ||
465 | |||
466 | // it would be great to allocate this array up front instead of | ||
467 | // throwing away hundreds of media segment fragments | ||
468 | boxes = new Uint8Array(moof.byteLength + mdat.byteLength); | ||
452 | 469 | ||
453 | // bump the sequence number for next time | 470 | // bump the sequence number for next time |
454 | sequenceNumber++; | 471 | sequenceNumber++; |
... | @@ -459,13 +476,41 @@ Transmuxer = function() { | ... | @@ -459,13 +476,41 @@ Transmuxer = function() { |
459 | self.trigger('data', { | 476 | self.trigger('data', { |
460 | data: boxes | 477 | data: boxes |
461 | }); | 478 | }); |
479 | }; | ||
480 | |||
481 | // handle incoming data events | ||
482 | h264Stream.on('data', function(data) { | ||
483 | // if this chunk starts a new access unit, flush the data we've been buffering | ||
484 | if (data.nalUnitType === 'access_unit_delimiter_rbsp' && | ||
485 | videoSamples.length) { | ||
486 | flushVideo(); | ||
487 | } | ||
488 | |||
489 | // buffer video until we encounter a new access unit (aka the next frame) | ||
490 | videoSamples.push(data); | ||
491 | videoSamplesSize += data.data.byteLength; | ||
492 | }); | ||
493 | programStream.on('data', function(data) { | ||
494 | // generate init segments based on stream metadata | ||
495 | if (!initialized && data.type === 'metadata') { | ||
496 | self.trigger('data', { | ||
497 | data: mp4.initSegment(data.tracks) | ||
498 | }); | ||
499 | initialized = true; | ||
500 | } | ||
462 | }); | 501 | }); |
502 | |||
463 | // feed incoming data to the front of the parsing pipeline | 503 | // feed incoming data to the front of the parsing pipeline |
464 | this.push = function(data) { | 504 | this.push = function(data) { |
465 | packetStream.push(data); | 505 | packetStream.push(data); |
466 | }; | 506 | }; |
467 | // flush any buffered data | 507 | // flush any buffered data |
468 | this.end = programStream.end; | 508 | this.end = function() { |
509 | programStream.end(); | ||
510 | if (videoSamples.length) { | ||
511 | flushVideo(); | ||
512 | } | ||
513 | }; | ||
469 | }; | 514 | }; |
470 | Transmuxer.prototype = new videojs.Hls.Stream(); | 515 | Transmuxer.prototype = new videojs.Hls.Stream(); |
471 | 516 | ... | ... |
... | @@ -41,7 +41,13 @@ test('generates a BSMFF ftyp', function() { | ... | @@ -41,7 +41,13 @@ test('generates a BSMFF ftyp', function() { |
41 | 41 | ||
42 | test('generates a moov', function() { | 42 | test('generates a moov', function() { |
43 | var boxes, mvhd, tkhd, mdhd, hdlr, minf, mvex, | 43 | var boxes, mvhd, tkhd, mdhd, hdlr, minf, mvex, |
44 | data = mp4.moov(100, 600, 300, "video"); | 44 | data = mp4.moov([{ |
45 | id: 7, | ||
46 | duration: 100, | ||
47 | width: 600, | ||
48 | height: 300, | ||
49 | type: 'video' | ||
50 | }]); | ||
45 | 51 | ||
46 | ok(data, 'box is not null'); | 52 | ok(data, 'box is not null'); |
47 | 53 | ||
... | @@ -53,12 +59,13 @@ test('generates a moov', function() { | ... | @@ -53,12 +59,13 @@ test('generates a moov', function() { |
53 | 59 | ||
54 | mvhd = boxes[0].boxes[0]; | 60 | mvhd = boxes[0].boxes[0]; |
55 | equal(mvhd.type, 'mvhd', 'generated a mvhd'); | 61 | equal(mvhd.type, 'mvhd', 'generated a mvhd'); |
56 | equal(mvhd.duration, 100, 'wrote the movie header duration'); | 62 | equal(mvhd.duration, 0xffffffff, 'wrote the maximum movie header duration'); |
57 | 63 | ||
58 | equal(boxes[0].boxes[1].type, 'trak', 'generated a trak'); | 64 | equal(boxes[0].boxes[1].type, 'trak', 'generated a trak'); |
59 | equal(boxes[0].boxes[1].boxes.length, 2, 'generated two track sub boxes'); | 65 | equal(boxes[0].boxes[1].boxes.length, 2, 'generated two track sub boxes'); |
60 | tkhd = boxes[0].boxes[1].boxes[0]; | 66 | tkhd = boxes[0].boxes[1].boxes[0]; |
61 | equal(tkhd.type, 'tkhd', 'generated a tkhd'); | 67 | equal(tkhd.type, 'tkhd', 'generated a tkhd'); |
68 | equal(tkhd.trackId, 7, 'wrote the track id'); | ||
62 | equal(tkhd.duration, 100, 'wrote duration into the track header'); | 69 | equal(tkhd.duration, 100, 'wrote duration into the track header'); |
63 | equal(tkhd.width, 600, 'wrote width into the track header'); | 70 | equal(tkhd.width, 600, 'wrote width into the track header'); |
64 | equal(tkhd.height, 300, 'wrote height into the track header'); | 71 | equal(tkhd.height, 300, 'wrote height into the track header'); |
... | @@ -69,7 +76,7 @@ test('generates a moov', function() { | ... | @@ -69,7 +76,7 @@ test('generates a moov', function() { |
69 | mdhd = boxes[0].boxes[1].boxes[1].boxes[0]; | 76 | mdhd = boxes[0].boxes[1].boxes[1].boxes[0]; |
70 | equal(mdhd.type, 'mdhd', 'generate an mdhd type'); | 77 | equal(mdhd.type, 'mdhd', 'generate an mdhd type'); |
71 | equal(mdhd.language, 'und', 'wrote undetermined language'); | 78 | equal(mdhd.language, 'und', 'wrote undetermined language'); |
72 | equal(mdhd.duration, 100, 'wrote duraiton into the media header'); | 79 | equal(mdhd.duration, 100, 'wrote duration into the media header'); |
73 | 80 | ||
74 | hdlr = boxes[0].boxes[1].boxes[1].boxes[1]; | 81 | hdlr = boxes[0].boxes[1].boxes[1].boxes[1]; |
75 | equal(hdlr.type, 'hdlr', 'generate an hdlr type'); | 82 | equal(hdlr.type, 'hdlr', 'generate an hdlr type'); |
... | @@ -206,7 +213,12 @@ test('generates a moov', function() { | ... | @@ -206,7 +213,12 @@ test('generates a moov', function() { |
206 | 213 | ||
207 | test('generates a sound hdlr', function() { | 214 | test('generates a sound hdlr', function() { |
208 | var boxes, hdlr, | 215 | var boxes, hdlr, |
209 | data = mp4.moov(100, 600, 300, "audio"); | 216 | data = mp4.moov([{ |
217 | duration:100, | ||
218 | width: 600, | ||
219 | height: 300, | ||
220 | type: 'audio' | ||
221 | }]); | ||
210 | 222 | ||
211 | ok(data, 'box is not null'); | 223 | ok(data, 'box is not null'); |
212 | 224 | ||
... | @@ -220,7 +232,12 @@ test('generates a sound hdlr', function() { | ... | @@ -220,7 +232,12 @@ test('generates a sound hdlr', function() { |
220 | 232 | ||
221 | test('generates a video hdlr', function() { | 233 | test('generates a video hdlr', function() { |
222 | var boxes, hdlr, | 234 | var boxes, hdlr, |
223 | data = mp4.moov(100, 600, 300, "video"); | 235 | data = mp4.moov([{ |
236 | duration: 100, | ||
237 | width: 600, | ||
238 | height: 300, | ||
239 | type: 'video' | ||
240 | }]); | ||
224 | 241 | ||
225 | ok(data, 'box is not null'); | 242 | ok(data, 'box is not null'); |
226 | 243 | ||
... | @@ -234,14 +251,40 @@ test('generates a video hdlr', function() { | ... | @@ -234,14 +251,40 @@ test('generates a video hdlr', function() { |
234 | 251 | ||
235 | test('generates an initialization segment', function() { | 252 | test('generates an initialization segment', function() { |
236 | var | 253 | var |
237 | data = mp4.initSegment(), | 254 | data = mp4.initSegment([{ |
238 | init; | 255 | id: 1, |
256 | width: 600, | ||
257 | height: 300, | ||
258 | type: 'video' | ||
259 | }, { | ||
260 | id: 2, | ||
261 | type: 'audio' | ||
262 | }]), | ||
263 | init, mvhd, trak1, trak2, mvex; | ||
239 | 264 | ||
240 | init = videojs.inspectMp4(data); | 265 | init = videojs.inspectMp4(data); |
241 | equal(init.length, 2, 'generated two boxes'); | 266 | equal(init.length, 2, 'generated two boxes'); |
242 | equal(init[0].type, 'ftyp', 'generated a ftyp box'); | 267 | equal(init[0].type, 'ftyp', 'generated a ftyp box'); |
243 | equal(init[1].type, 'moov', 'generated a moov box'); | 268 | equal(init[1].type, 'moov', 'generated a moov box'); |
244 | equal(init[1].boxes[0].duration, 0xffffffff, 'wrote a maximum duration'); | 269 | equal(init[1].boxes[0].duration, 0xffffffff, 'wrote a maximum duration'); |
270 | |||
271 | mvhd = init[1].boxes[0]; | ||
272 | equal(mvhd.type, 'mvhd', 'wrote an mvhd'); | ||
273 | |||
274 | trak1 = init[1].boxes[1]; | ||
275 | equal(trak1.type, 'trak', 'wrote a trak'); | ||
276 | equal(trak1.boxes[0].trackId, 1, 'wrote the first track id'); | ||
277 | equal(trak1.boxes[0].width, 600, 'wrote the first track width'); | ||
278 | equal(trak1.boxes[0].height, 300, 'wrote the first track height'); | ||
279 | equal(trak1.boxes[1].boxes[1].handlerType, 'vide', 'wrote the first track type'); | ||
280 | |||
281 | trak2 = init[1].boxes[2]; | ||
282 | equal(trak2.type, 'trak', 'wrote a trak'); | ||
283 | equal(trak2.boxes[0].trackId, 2, 'wrote the second track id'); | ||
284 | equal(trak2.boxes[1].boxes[1].handlerType, 'soun', 'wrote the second track type'); | ||
285 | |||
286 | mvex = init[1].boxes[3]; | ||
287 | equal(mvex.type, 'mvex', 'wrote an mvex'); | ||
245 | }); | 288 | }); |
246 | 289 | ||
247 | test('generates a minimal moof', function() { | 290 | test('generates a minimal moof', function() { | ... | ... |
... | @@ -27,6 +27,8 @@ var | ... | @@ -27,6 +27,8 @@ var |
27 | parseStream, | 27 | parseStream, |
28 | ProgramStream = videojs.mp2t.ProgramStream, | 28 | ProgramStream = videojs.mp2t.ProgramStream, |
29 | programStream, | 29 | programStream, |
30 | H264Stream = videojs.mp2t.H264Stream, | ||
31 | h264Stream, | ||
30 | Transmuxer = videojs.mp2t.Transmuxer, | 32 | Transmuxer = videojs.mp2t.Transmuxer, |
31 | transmuxer, | 33 | transmuxer, |
32 | 34 | ||
... | @@ -37,7 +39,9 @@ var | ... | @@ -37,7 +39,9 @@ var |
37 | 39 | ||
38 | PAT, | 40 | PAT, |
39 | PMT, | 41 | PMT, |
40 | standalonePes; | 42 | standalonePes, |
43 | |||
44 | videoPes; | ||
41 | 45 | ||
42 | module('MP2T Packet Stream', { | 46 | module('MP2T Packet Stream', { |
43 | setup: function() { | 47 | setup: function() { |
... | @@ -261,8 +265,8 @@ PMT = [ | ... | @@ -261,8 +265,8 @@ PMT = [ |
261 | 0x40, 0x10, | 265 | 0x40, 0x10, |
262 | // tsc:01 afc:01 cc:0000 pointer_field:0000 0000 | 266 | // tsc:01 afc:01 cc:0000 pointer_field:0000 0000 |
263 | 0x50, 0x00, | 267 | 0x50, 0x00, |
264 | // tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0010 1111 | 268 | // tid:0000 0010 ssi:0 0:0 r:00 sl:0000 0001 0111 |
265 | 0x00, 0x00, 0x2f, | 269 | 0x02, 0x00, 0x17, |
266 | // pn:0000 0000 0000 0001 | 270 | // pn:0000 0000 0000 0001 |
267 | 0x00, 0x01, | 271 | 0x00, 0x01, |
268 | // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000 | 272 | // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000 |
... | @@ -292,12 +296,13 @@ test('parse the elementary streams from a program map table', function() { | ... | @@ -292,12 +296,13 @@ test('parse the elementary streams from a program map table', function() { |
292 | }); | 296 | }); |
293 | parseStream.pmtPid = 0x0010; | 297 | parseStream.pmtPid = 0x0010; |
294 | 298 | ||
295 | parseStream.push(new Uint8Array(PMT)); | 299 | parseStream.push(new Uint8Array(PMT.concat(0, 0, 0, 0, 0))); |
296 | 300 | ||
297 | ok(packet, 'parsed a packet'); | 301 | ok(packet, 'parsed a packet'); |
298 | ok(parseStream.programMapTable, 'parsed a program map'); | 302 | ok(parseStream.programMapTable, 'parsed a program map'); |
299 | strictEqual(0x1b, parseStream.programMapTable[0x11], 'associated h264 with pid 0x11'); | 303 | strictEqual(0x1b, parseStream.programMapTable[0x11], 'associated h264 with pid 0x11'); |
300 | strictEqual(0x0f, parseStream.programMapTable[0x12], 'associated adts with pid 0x12'); | 304 | strictEqual(0x0f, parseStream.programMapTable[0x12], 'associated adts with pid 0x12'); |
305 | strictEqual(parseStream.programMapTable[0], undefined, 'ignored trailing stuffing bytes'); | ||
301 | deepEqual(parseStream.programMapTable, packet.programMapTable, 'recorded the PMT'); | 306 | deepEqual(parseStream.programMapTable, packet.programMapTable, 'recorded the PMT'); |
302 | }); | 307 | }); |
303 | 308 | ||
... | @@ -386,57 +391,62 @@ test('parses an elementary stream packet with a pts and dts', function() { | ... | @@ -386,57 +391,62 @@ test('parses an elementary stream packet with a pts and dts', function() { |
386 | equal(2 / 90, packet.dts, 'parsed the dts'); | 391 | equal(2 / 90, packet.dts, 'parsed the dts'); |
387 | }); | 392 | }); |
388 | 393 | ||
389 | standalonePes = [ | 394 | // helper function to create video PES packets |
390 | 0x47, // sync byte | 395 | videoPes = function(data) { |
391 | // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001 | 396 | if (data.length !== 2) { |
392 | 0x40, 0x11, | 397 | throw new Error('video PES only accepts 2 byte payloads'); |
393 | // tsc:01 afc:11 cc:0000 | 398 | } |
394 | 0x70, | 399 | return [ |
395 | // afl:1010 1100 | 400 | 0x47, // sync byte |
396 | 0xac, | 401 | // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001 |
397 | // di:0 rai:0 espi:0 pf:0 of:0 spf:0 tpdf:0 afef:0 | 402 | 0x40, 0x11, |
398 | 0x00, | 403 | // tsc:01 afc:11 cc:0000 |
399 | // stuffing_bytes (171 bytes) | 404 | 0x70, |
400 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 405 | // afl:1010 1100 |
401 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 406 | 0xac, |
402 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 407 | // di:0 rai:0 espi:0 pf:0 of:0 spf:0 tpdf:0 afef:0 |
403 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 408 | 0x00, |
404 | 409 | // stuffing_bytes (171 bytes) | |
405 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 410 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
406 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 411 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
407 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 412 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
408 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 413 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
409 | 414 | ||
410 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 415 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
411 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 416 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
412 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 417 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
413 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 418 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
414 | 419 | ||
415 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 420 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
416 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 421 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
417 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 422 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
418 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 423 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
419 | 424 | ||
420 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 425 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
421 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 426 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
422 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 427 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
423 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 428 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
424 | 429 | ||
425 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | 430 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
426 | 0xff, 0xff, 0xff, | 431 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
427 | // pscp:0000 0000 0000 0000 0000 0001 | 432 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
428 | 0x00, 0x00, 0x01, | 433 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
429 | // sid:0000 0000 ppl:0000 0000 0000 0101 | 434 | |
430 | 0x00, 0x00, 0x05, | 435 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
431 | // 10 psc:00 pp:0 dai:1 c:0 ooc:0 | 436 | 0xff, 0xff, 0xff, |
432 | 0x84, | 437 | // pscp:0000 0000 0000 0000 0000 0001 |
433 | // pdf:00 ef:1 erf:0 dtmf:0 acif:0 pcf:0 pef:0 | 438 | 0x00, 0x00, 0x01, |
434 | 0x20, | 439 | // sid:0000 0000 ppl:0000 0000 0000 0101 |
435 | // phdl:0000 0000 | 440 | 0x00, 0x00, 0x05, |
436 | 0x00, | 441 | // 10 psc:00 pp:0 dai:1 c:0 ooc:0 |
437 | // "data":1010 1111 0000 0001 | 442 | 0x84, |
438 | 0xaf, 0x01 | 443 | // pdf:00 ef:1 erf:0 dtmf:0 acif:0 pcf:0 pef:0 |
439 | ]; | 444 | 0x20, |
445 | // phdl:0000 0000 | ||
446 | 0x00 | ||
447 | ].concat(data); | ||
448 | }; | ||
449 | standalonePes = videoPes([0xaf, 0x01]); | ||
440 | 450 | ||
441 | test('parses an elementary stream packet without a pts or dts', function() { | 451 | test('parses an elementary stream packet without a pts or dts', function() { |
442 | 452 | ||
... | @@ -503,10 +513,12 @@ test('parses metadata events from PSI packets', function() { | ... | @@ -503,10 +513,12 @@ test('parses metadata events from PSI packets', function() { |
503 | metadatas[0].tracks.sort(sortById); | 513 | metadatas[0].tracks.sort(sortById); |
504 | deepEqual(metadatas[0].tracks, [{ | 514 | deepEqual(metadatas[0].tracks, [{ |
505 | id: 1, | 515 | id: 1, |
506 | codec: 'avc' | 516 | codec: 'avc', |
517 | type: 'video' | ||
507 | }, { | 518 | }, { |
508 | id: 2, | 519 | id: 2, |
509 | codec: 'adts' | 520 | codec: 'adts', |
521 | type: 'audio' | ||
510 | }], 'identified two tracks'); | 522 | }], 'identified two tracks'); |
511 | }); | 523 | }); |
512 | 524 | ||
... | @@ -628,6 +640,26 @@ test('flushes the buffered packets when a new one of that type is started', func | ... | @@ -628,6 +640,26 @@ test('flushes the buffered packets when a new one of that type is started', func |
628 | equal(7, packets[2].data.byteLength, 'parsed the audio payload'); | 640 | equal(7, packets[2].data.byteLength, 'parsed the audio payload'); |
629 | }); | 641 | }); |
630 | 642 | ||
643 | module('H264 Stream', { | ||
644 | setup: function() { | ||
645 | h264Stream = new H264Stream(); | ||
646 | } | ||
647 | }); | ||
648 | test('parses nal unit types', function() { | ||
649 | var data; | ||
650 | h264Stream.on('data', function(event) { | ||
651 | data = event; | ||
652 | }); | ||
653 | |||
654 | h264Stream.push({ | ||
655 | type: 'video', | ||
656 | data: new Uint8Array([0x09]) | ||
657 | }); | ||
658 | |||
659 | ok(data, 'generated a data event'); | ||
660 | equal(data.nalUnitType, 'access_unit_delimiter_rbsp', 'identified an access unit delimiter'); | ||
661 | }); | ||
662 | |||
631 | module('Transmuxer', { | 663 | module('Transmuxer', { |
632 | setup: function() { | 664 | setup: function() { |
633 | transmuxer = new Transmuxer(); | 665 | transmuxer = new Transmuxer(); |
... | @@ -635,11 +667,48 @@ module('Transmuxer', { | ... | @@ -635,11 +667,48 @@ module('Transmuxer', { |
635 | }); | 667 | }); |
636 | 668 | ||
637 | test('generates an init segment', function() { | 669 | test('generates an init segment', function() { |
670 | var segments = []; | ||
671 | transmuxer.on('data', function(segment) { | ||
672 | segments.push(segment); | ||
673 | }); | ||
638 | transmuxer.push(packetize(PAT)); | 674 | transmuxer.push(packetize(PAT)); |
639 | transmuxer.push(packetize(PMT)); | 675 | transmuxer.push(packetize(PMT)); |
640 | transmuxer.push(packetize(standalonePes)); | 676 | transmuxer.push(packetize(standalonePes)); |
677 | transmuxer.end(); | ||
641 | 678 | ||
642 | ok(transmuxer.initSegment, 'has an init segment'); | 679 | equal(segments.length, 2, 'has an init segment'); |
680 | }); | ||
681 | |||
682 | test('buffers video samples until an access unit', function() { | ||
683 | var samples = [], boxes; | ||
684 | transmuxer.on('data', function(data) { | ||
685 | samples.push(data); | ||
686 | }); | ||
687 | transmuxer.push(packetize(PAT)); | ||
688 | transmuxer.push(packetize(PMT)); | ||
689 | |||
690 | // buffer a NAL | ||
691 | transmuxer.push(packetize(videoPes([0x09, 0x01]))); | ||
692 | transmuxer.push(packetize(videoPes([0x00, 0x02]))); | ||
693 | |||
694 | // an access_unit_delimiter_rbsp should flush the buffer | ||
695 | transmuxer.push(packetize(videoPes([0x09, 0x03]))); | ||
696 | transmuxer.push(packetize(videoPes([0x00, 0x04]))); | ||
697 | equal(samples.length, 2, 'emitted two events'); | ||
698 | boxes = videojs.inspectMp4(samples[1].data); | ||
699 | equal(boxes.length, 2, 'generated two boxes'); | ||
700 | equal(boxes[0].type, 'moof', 'the first box is a moof'); | ||
701 | equal(boxes[1].type, 'mdat', 'the second box is a mdat'); | ||
702 | deepEqual(new Uint8Array(samples[1].data.subarray(samples[1].data.length - 4)), | ||
703 | new Uint8Array([0x09, 0x01, 0x00, 0x02]), | ||
704 | 'concatenated NALs into an mdat'); | ||
705 | |||
706 | // flush the last access unit | ||
707 | transmuxer.end(); | ||
708 | equal(samples.length, 3, 'flushed the final access unit'); | ||
709 | deepEqual(new Uint8Array(samples[2].data.subarray(samples[2].data.length - 4)), | ||
710 | new Uint8Array([0x09, 0x03, 0x00, 0x04]), | ||
711 | 'concatenated NALs into an mdat'); | ||
643 | }); | 712 | }); |
644 | 713 | ||
645 | test('parses an example mp2t file and generates media segments', function() { | 714 | test('parses an example mp2t file and generates media segments', function() { |
... | @@ -653,23 +722,32 @@ test('parses an example mp2t file and generates media segments', function() { | ... | @@ -653,23 +722,32 @@ test('parses an example mp2t file and generates media segments', function() { |
653 | transmuxer.push(window.bcSegment); | 722 | transmuxer.push(window.bcSegment); |
654 | transmuxer.end(); | 723 | transmuxer.end(); |
655 | 724 | ||
656 | ok(segments.length, 'generated media segments'); | 725 | equal(segments.length, 2, 'generated two segments'); |
657 | i = segments.length; | 726 | |
658 | while (i--) { | 727 | boxes = videojs.inspectMp4(segments[0].data); |
659 | boxes = videojs.inspectMp4(segments[i].data); | 728 | equal(boxes.length, 2, 'init segments are composed of two boxes'); |
660 | equal(boxes.length, 2, 'segments are composed of two boxes'); | 729 | equal(boxes[0].type, 'ftyp', 'the first box is an ftyp'); |
661 | equal(boxes[0].type, 'moof', 'first box is a moof'); | 730 | equal(boxes[1].type, 'moov', 'the second box is a moov'); |
662 | equal(boxes[0].boxes.length, 2, 'the moof has two children'); | 731 | equal(boxes[1].boxes[0].type, 'mvhd', 'generated an mvhd'); |
663 | 732 | equal(boxes[1].boxes[1].type, 'trak', 'generated a trak'); | |
664 | mfhd = boxes[0].boxes[0]; | 733 | equal(boxes[1].boxes[2].type, 'trak', 'generated a second trak'); |
734 | equal(boxes[1].boxes[3].type, 'mvex', 'generated an mvex'); | ||
735 | |||
736 | boxes = videojs.inspectMp4(segments[1].data); | ||
737 | ok(boxes.length > 0, 'media segments are not empty'); | ||
738 | ok(boxes.length % 2 === 0, 'media segments are composed of pairs of boxes'); | ||
739 | for (i = 0; i < boxes.length; i += 2) { | ||
740 | equal(boxes[i].type, 'moof', 'first box is a moof'); | ||
741 | equal(boxes[i].boxes.length, 2, 'the moof has two children'); | ||
742 | |||
743 | mfhd = boxes[i].boxes[0]; | ||
665 | equal(mfhd.type, 'mfhd', 'mfhd is a child of the moof'); | 744 | equal(mfhd.type, 'mfhd', 'mfhd is a child of the moof'); |
666 | ok(mfhd.sequenceNumber < sequenceNumber, 'sequence numbers are increasing'); | 745 | ok(mfhd.sequenceNumber < sequenceNumber, 'sequence numbers are increasing'); |
667 | sequenceNumber = mfhd.sequenceNumber; | 746 | sequenceNumber = mfhd.sequenceNumber; |
668 | 747 | ||
669 | traf = boxes[0].boxes[1]; | 748 | traf = boxes[i].boxes[1]; |
670 | equal(traf.type, 'traf', 'traf is a child of the moof'); | 749 | equal(traf.type, 'traf', 'traf is a child of the moof'); |
671 | 750 | equal(boxes[i + 1].type, 'mdat', 'second box is an mdat'); | |
672 | equal(boxes[1].type, 'mdat', 'second box is an mdat'); | ||
673 | } | 751 | } |
674 | }); | 752 | }); |
675 | 753 | ... | ... |
-
Please register or sign in to post a comment