Generate a valid audio initialization segment
Modify the mp4 generator to inspect audio tracks and generate a working initialization segment. Hook the audio init segment up to the mp4 transmuxing test page.
Showing
9 changed files
with
831 additions
and
256 deletions
... | @@ -4,7 +4,7 @@ | ... | @@ -4,7 +4,7 @@ |
4 | var box, dinf, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, | 4 | var box, dinf, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, |
5 | tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, styp, traf, trex, trun, | 5 | tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, styp, traf, trex, trun, |
6 | types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, | 6 | types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, |
7 | AUDIO_HDLR, HDLR_TYPES, VMHD, DREF, STCO, STSC, STSZ, STTS, | 7 | AUDIO_HDLR, HDLR_TYPES, ESDS, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS, |
8 | Uint8Array, DataView; | 8 | Uint8Array, DataView; |
9 | 9 | ||
10 | Uint8Array = window.Uint8Array; | 10 | Uint8Array = window.Uint8Array; |
... | @@ -19,6 +19,7 @@ DataView = window.DataView; | ... | @@ -19,6 +19,7 @@ DataView = window.DataView; |
19 | btrt: [], | 19 | btrt: [], |
20 | dinf: [], | 20 | dinf: [], |
21 | dref: [], | 21 | dref: [], |
22 | esds: [], | ||
22 | ftyp: [], | 23 | ftyp: [], |
23 | hdlr: [], | 24 | hdlr: [], |
24 | mdat: [], | 25 | mdat: [], |
... | @@ -28,9 +29,11 @@ DataView = window.DataView; | ... | @@ -28,9 +29,11 @@ DataView = window.DataView; |
28 | minf: [], | 29 | minf: [], |
29 | moof: [], | 30 | moof: [], |
30 | moov: [], | 31 | moov: [], |
32 | mp4a: [], // codingname | ||
31 | mvex: [], | 33 | mvex: [], |
32 | mvhd: [], | 34 | mvhd: [], |
33 | sdtp: [], | 35 | sdtp: [], |
36 | smhd: [], | ||
34 | stbl: [], | 37 | stbl: [], |
35 | stco: [], | 38 | stco: [], |
36 | stsc: [], | 39 | stsc: [], |
... | @@ -109,6 +112,39 @@ DataView = window.DataView; | ... | @@ -109,6 +112,39 @@ DataView = window.DataView; |
109 | 0x00, // version 0 | 112 | 0x00, // version 0 |
110 | 0x00, 0x00, 0x01 // entry_flags | 113 | 0x00, 0x00, 0x01 // entry_flags |
111 | ]); | 114 | ]); |
115 | ESDS = new Uint8Array([ | ||
116 | 0x00, // version | ||
117 | 0x00, 0x00, 0x00, // flags | ||
118 | |||
119 | // ES_Descriptor | ||
120 | 0x03, // tag, ES_DescrTag | ||
121 | 0x19, // length | ||
122 | 0x00, 0x00, // ES_ID | ||
123 | 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority | ||
124 | |||
125 | // DecoderConfigDescriptor | ||
126 | 0x04, // tag, DecoderConfigDescrTag | ||
127 | 0x11, // length | ||
128 | 0x40, // object type | ||
129 | 0x15, // streamType | ||
130 | 0x00, 0x06, 0x00, // bufferSizeDB | ||
131 | 0x00, 0x00, 0xda, 0xc0, // maxBitrate | ||
132 | 0x00, 0x00, 0xda, 0xc0, // avgBitrate | ||
133 | |||
134 | // DecoderSpecificInfo | ||
135 | 0x05, // tag, DecoderSpecificInfoTag | ||
136 | 0x02, // length | ||
137 | // ISO/IEC 14496-3, AudioSpecificConfig | ||
138 | 0x11, // AudioObjectType, AAC LC. | ||
139 | 0x90, // samplingFrequencyIndex, 8 -> 16000. channelConfig, 2 -> stereo. | ||
140 | 0x06, 0x01, 0x02 // GASpecificConfig | ||
141 | ]); | ||
142 | SMHD = new Uint8Array([ | ||
143 | 0x00, // version | ||
144 | 0x00, 0x00, 0x00, // flags | ||
145 | 0x00, 0x00, // balance, 0 means centered | ||
146 | 0x00, 0x00 // reserved | ||
147 | ]); | ||
112 | STCO = new Uint8Array([ | 148 | STCO = new Uint8Array([ |
113 | 0x00, // version | 149 | 0x00, // version |
114 | 0x00, 0x00, 0x00, // flags | 150 | 0x00, 0x00, 0x00, // flags |
... | @@ -171,24 +207,35 @@ hdlr = function(type) { | ... | @@ -171,24 +207,35 @@ hdlr = function(type) { |
171 | mdat = function(data) { | 207 | mdat = function(data) { |
172 | return box(types.mdat, data); | 208 | return box(types.mdat, data); |
173 | }; | 209 | }; |
174 | mdhd = function(duration) { | 210 | mdhd = function(track) { |
175 | return box(types.mdhd, new Uint8Array([ | 211 | var result = new Uint8Array([ |
176 | 0x00, // version 0 | 212 | 0x00, // version 0 |
177 | 0x00, 0x00, 0x00, // flags | 213 | 0x00, 0x00, 0x00, // flags |
178 | 0x00, 0x00, 0x00, 0x02, // creation_time | 214 | 0x00, 0x00, 0x00, 0x02, // creation_time |
179 | 0x00, 0x00, 0x00, 0x03, // modification_time | 215 | 0x00, 0x00, 0x00, 0x03, // modification_time |
180 | 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second | 216 | 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second |
181 | 217 | ||
182 | (duration & 0xFF000000) >> 24, | 218 | (track.duration >>> 24), |
183 | (duration & 0xFF0000) >> 16, | 219 | (track.duration >>> 16) & 0xFF, |
184 | (duration & 0xFF00) >> 8, | 220 | (track.duration >>> 8) & 0xFF, |
185 | duration & 0xFF, // duration | 221 | track.duration & 0xFF, // duration |
186 | 0x55, 0xc4, // 'und' language (undetermined) | 222 | 0x55, 0xc4, // 'und' language (undetermined) |
187 | 0x00, 0x00 | 223 | 0x00, 0x00 |
188 | ])); | 224 | ]); |
225 | |||
226 | // Use the sample rate from the track metadata, when it is | ||
227 | // defined. The sample rate can be parsed out of an ADTS header, for | ||
228 | // instance. | ||
229 | if (track.samplerate) { | ||
230 | result[12] = (track.samplerate >>> 24); | ||
231 | result[13] = (track.samplerate >>> 16) & 0xFF; | ||
232 | result[14] = (track.samplerate >>> 8) & 0xFF; | ||
233 | result[15] = (track.samplerate) & 0xFF; | ||
234 | } | ||
235 | return box(types.mdhd, result); | ||
189 | }; | 236 | }; |
190 | mdia = function(track) { | 237 | mdia = function(track) { |
191 | return box(types.mdia, mdhd(track.duration), hdlr(track.type), minf(track)); | 238 | return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); |
192 | }; | 239 | }; |
193 | mfhd = function(sequenceNumber) { | 240 | mfhd = function(sequenceNumber) { |
194 | return box(types.mfhd, new Uint8Array([ | 241 | return box(types.mfhd, new Uint8Array([ |
... | @@ -201,7 +248,10 @@ mfhd = function(sequenceNumber) { | ... | @@ -201,7 +248,10 @@ mfhd = function(sequenceNumber) { |
201 | ])); | 248 | ])); |
202 | }; | 249 | }; |
203 | minf = function(track) { | 250 | minf = function(track) { |
204 | return box(types.minf, box(types.vmhd, VMHD), dinf(), stbl(track)); | 251 | return box(types.minf, |
252 | track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), | ||
253 | dinf(), | ||
254 | stbl(track)); | ||
205 | }; | 255 | }; |
206 | moof = function(sequenceNumber, tracks) { | 256 | moof = function(sequenceNumber, tracks) { |
207 | var | 257 | var |
... | @@ -217,7 +267,9 @@ moof = function(sequenceNumber, tracks) { | ... | @@ -217,7 +267,9 @@ moof = function(sequenceNumber, tracks) { |
217 | ].concat(trackFragments)); | 267 | ].concat(trackFragments)); |
218 | }; | 268 | }; |
219 | /** | 269 | /** |
220 | * @param tracks... (optional) {array} the tracks associated with this movie | 270 | * Returns a movie box. |
271 | * @param tracks {array} the tracks associated with this movie | ||
272 | * @see ISO/IEC 14496-12:2012(E), section 8.2.1 | ||
221 | */ | 273 | */ |
222 | moov = function(tracks) { | 274 | moov = function(tracks) { |
223 | var | 275 | var |
... | @@ -307,32 +359,36 @@ stbl = function(track) { | ... | @@ -307,32 +359,36 @@ stbl = function(track) { |
307 | box(types.stco, STCO)); | 359 | box(types.stco, STCO)); |
308 | }; | 360 | }; |
309 | 361 | ||
310 | stsd = function(track) { | 362 | (function() { |
311 | var sequenceParameterSets = [], pictureParameterSets = [], i; | 363 | var videoSample, audioSample; |
312 | 364 | ||
313 | if (track.type === 'audio') { | 365 | stsd = function(track) { |
314 | return box(types.stsd); | ||
315 | } | ||
316 | 366 | ||
317 | // assemble the SPSs | 367 | return box(types.stsd, new Uint8Array([ |
318 | for (i = 0; i < track.sps.length; i++) { | 368 | 0x00, // version 0 |
319 | sequenceParameterSets.push((track.sps[i].byteLength & 0xFF00) >>> 8); | 369 | 0x00, 0x00, 0x00, // flags |
320 | sequenceParameterSets.push((track.sps[i].byteLength & 0xFF)); // sequenceParameterSetLength | 370 | 0x00, 0x00, 0x00, 0x01 |
321 | sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(track.sps[i])); // SPS | 371 | ]), track.type === 'video' ? videoSample(track) : audioSample(track)); |
322 | } | 372 | }; |
323 | 373 | ||
324 | // assemble the PPSs | 374 | videoSample = function(track) { |
325 | for (i = 0; i < track.pps.length; i++) { | 375 | var sequenceParameterSets = [], pictureParameterSets = [], i; |
326 | pictureParameterSets.push((track.pps[i].byteLength & 0xFF00) >>> 8); | ||
327 | pictureParameterSets.push((track.pps[i].byteLength & 0xFF)); | ||
328 | pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(track.pps[i])); | ||
329 | } | ||
330 | 376 | ||
331 | return box(types.stsd, new Uint8Array([ | 377 | // assemble the SPSs |
332 | 0x00, // version 0 | 378 | for (i = 0; i < track.sps.length; i++) { |
333 | 0x00, 0x00, 0x00, // flags | 379 | sequenceParameterSets.push((track.sps[i].byteLength & 0xFF00) >>> 8); |
334 | 0x00, 0x00, 0x00, 0x01]), | 380 | sequenceParameterSets.push((track.sps[i].byteLength & 0xFF)); // sequenceParameterSetLength |
335 | box(types.avc1, new Uint8Array([ | 381 | sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(track.sps[i])); // SPS |
382 | } | ||
383 | |||
384 | // assemble the PPSs | ||
385 | for (i = 0; i < track.pps.length; i++) { | ||
386 | pictureParameterSets.push((track.pps[i].byteLength & 0xFF00) >>> 8); | ||
387 | pictureParameterSets.push((track.pps[i].byteLength & 0xFF)); | ||
388 | pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(track.pps[i])); | ||
389 | } | ||
390 | |||
391 | return box(types.avc1, new Uint8Array([ | ||
336 | 0x00, 0x00, 0x00, | 392 | 0x00, 0x00, 0x00, |
337 | 0x00, 0x00, 0x00, // reserved | 393 | 0x00, 0x00, 0x00, // reserved |
338 | 0x00, 0x01, // data_reference_index | 394 | 0x00, 0x01, // data_reference_index |
... | @@ -359,31 +415,60 @@ stsd = function(track) { | ... | @@ -359,31 +415,60 @@ stsd = function(track) { |
359 | 0x00, 0x00, 0x00, 0x00, | 415 | 0x00, 0x00, 0x00, 0x00, |
360 | 0x00, 0x00, 0x00, // compressorname | 416 | 0x00, 0x00, 0x00, // compressorname |
361 | 0x00, 0x18, // depth = 24 | 417 | 0x00, 0x18, // depth = 24 |
362 | 0x11, 0x11]), // pre_defined = -1 | 418 | 0x11, 0x11 // pre_defined = -1 |
363 | box(types.avcC, new Uint8Array([ | 419 | ]), box(types.avcC, new Uint8Array([ |
364 | 0x01, // configurationVersion | 420 | 0x01, // configurationVersion |
365 | track.profileIdc, // AVCProfileIndication | 421 | track.profileIdc, // AVCProfileIndication |
366 | track.profileCompatibility, // profile_compatibility | 422 | track.profileCompatibility, // profile_compatibility |
367 | track.levelIdc, // AVCLevelIndication | 423 | track.levelIdc, // AVCLevelIndication |
368 | 0xff // lengthSizeMinusOne, hard-coded to 4 bytes | 424 | 0xff // lengthSizeMinusOne, hard-coded to 4 bytes |
369 | ].concat([ | 425 | ].concat([ |
370 | track.sps.length // numOfSequenceParameterSets | 426 | track.sps.length // numOfSequenceParameterSets |
371 | ]).concat(sequenceParameterSets).concat([ | 427 | ]).concat(sequenceParameterSets).concat([ |
372 | track.pps.length // numOfPictureParameterSets | 428 | track.pps.length // numOfPictureParameterSets |
373 | ]).concat(pictureParameterSets))), // "PPS" | 429 | ]).concat(pictureParameterSets))), // "PPS" |
374 | box(types.btrt, new Uint8Array([ | 430 | box(types.btrt, new Uint8Array([ |
375 | 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB | 431 | 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB |
376 | 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate | 432 | 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate |
377 | 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate | 433 | 0x00, 0x2d, 0xc6, 0xc0 |
378 | )); | 434 | ])) // avgBitrate |
379 | }; | 435 | ); |
436 | }; | ||
437 | |||
438 | audioSample = function(track) { | ||
439 | return box(types.mp4a, new Uint8Array([ | ||
440 | |||
441 | // SampleEntry, ISO/IEC 14496-12 | ||
442 | 0x00, 0x00, 0x00, | ||
443 | 0x00, 0x00, 0x00, // reserved | ||
444 | 0x00, 0x01, // data_reference_index | ||
445 | |||
446 | // AudioSampleEntry, ISO/IEC 14496-12 | ||
447 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
448 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
449 | (track.channelcount & 0xff00) >> 8, | ||
450 | (track.channelcount & 0xff), // channelcount | ||
451 | |||
452 | (track.samplesize & 0xff00) >> 8, | ||
453 | (track.samplesize & 0xff), // samplesize | ||
454 | 0x00, 0x00, // pre_defined | ||
455 | 0x00, 0x00, // reserved | ||
456 | |||
457 | (track.samplerate & 0xff00) >> 8, | ||
458 | (track.samplerate & 0xff), | ||
459 | 0x00, 0x00 // samplerate, 16.16 | ||
460 | |||
461 | // MP4AudioSampleEntry, ISO/IEC 14496-14 | ||
462 | ]), box(types.esds, ESDS)); | ||
463 | }; | ||
464 | })(); | ||
380 | 465 | ||
381 | styp = function() { | 466 | styp = function() { |
382 | return box(types.styp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND); | 467 | return box(types.styp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND); |
383 | }; | 468 | }; |
384 | 469 | ||
385 | tkhd = function(track) { | 470 | tkhd = function(track) { |
386 | return box(types.tkhd, new Uint8Array([ | 471 | var result = new Uint8Array([ |
387 | 0x00, // version 0 | 472 | 0x00, // version 0 |
388 | 0x00, 0x00, 0x07, // flags | 473 | 0x00, 0x00, 0x07, // flags |
389 | 0x00, 0x00, 0x00, 0x00, // creation_time | 474 | 0x00, 0x00, 0x00, 0x00, // creation_time |
... | @@ -401,7 +486,7 @@ tkhd = function(track) { | ... | @@ -401,7 +486,7 @@ tkhd = function(track) { |
401 | 0x00, 0x00, 0x00, 0x00, // reserved | 486 | 0x00, 0x00, 0x00, 0x00, // reserved |
402 | 0x00, 0x00, // layer | 487 | 0x00, 0x00, // layer |
403 | 0x00, 0x00, // alternate_group | 488 | 0x00, 0x00, // alternate_group |
404 | 0x00, 0x00, // non-audio track volume | 489 | 0x01, 0x00, // non-audio track volume |
405 | 0x00, 0x00, // reserved | 490 | 0x00, 0x00, // reserved |
406 | 0x00, 0x01, 0x00, 0x00, | 491 | 0x00, 0x01, 0x00, 0x00, |
407 | 0x00, 0x00, 0x00, 0x00, | 492 | 0x00, 0x00, 0x00, 0x00, |
... | @@ -418,7 +503,9 @@ tkhd = function(track) { | ... | @@ -418,7 +503,9 @@ tkhd = function(track) { |
418 | (track.height & 0xFF00) >> 8, | 503 | (track.height & 0xFF00) >> 8, |
419 | track.height & 0xFF, | 504 | track.height & 0xFF, |
420 | 0x00, 0x00 // height | 505 | 0x00, 0x00 // height |
421 | ])); | 506 | ]); |
507 | |||
508 | return box(types.tkhd, result); | ||
422 | }; | 509 | }; |
423 | 510 | ||
424 | traf = function(track) { | 511 | traf = function(track) { |
... | @@ -461,7 +548,7 @@ trak = function(track) { | ... | @@ -461,7 +548,7 @@ trak = function(track) { |
461 | }; | 548 | }; |
462 | 549 | ||
463 | trex = function(track) { | 550 | trex = function(track) { |
464 | return box(types.trex, new Uint8Array([ | 551 | var result = new Uint8Array([ |
465 | 0x00, // version 0 | 552 | 0x00, // version 0 |
466 | 0x00, 0x00, 0x00, // flags | 553 | 0x00, 0x00, 0x00, // flags |
467 | (track.id & 0xFF000000) >> 24, | 554 | (track.id & 0xFF000000) >> 24, |
... | @@ -472,7 +559,16 @@ trex = function(track) { | ... | @@ -472,7 +559,16 @@ trex = function(track) { |
472 | 0x00, 0x00, 0x00, 0x00, // default_sample_duration | 559 | 0x00, 0x00, 0x00, 0x00, // default_sample_duration |
473 | 0x00, 0x00, 0x00, 0x00, // default_sample_size | 560 | 0x00, 0x00, 0x00, 0x00, // default_sample_size |
474 | 0x00, 0x01, 0x00, 0x01 // default_sample_flags | 561 | 0x00, 0x01, 0x00, 0x01 // default_sample_flags |
475 | ])); | 562 | ]); |
563 | // the last two bytes of default_sample_flags is the sample | ||
564 | // degradation priority, a hint about the importance of this sample | ||
565 | // relative to others. Lower the degradation priority for all sample | ||
566 | // types other than video. | ||
567 | if (track.type !== 'video') { | ||
568 | result[result.length - 1] = 0x00; | ||
569 | } | ||
570 | |||
571 | return box(types.trex, result); | ||
476 | }; | 572 | }; |
477 | 573 | ||
478 | trun = function(track, offset) { | 574 | trun = function(track, offset) { | ... | ... |
... | @@ -16,12 +16,29 @@ | ... | @@ -16,12 +16,29 @@ |
16 | 16 | ||
17 | var | 17 | var |
18 | TransportPacketStream, TransportParseStream, ElementaryStream, VideoSegmentStream, | 18 | TransportPacketStream, TransportParseStream, ElementaryStream, VideoSegmentStream, |
19 | Transmuxer, AacStream, H264Stream, NalByteStream, | 19 | AudioSegmentStream, Transmuxer, AacStream, H264Stream, NalByteStream, |
20 | MP2T_PACKET_LENGTH, H264_STREAM_TYPE, ADTS_STREAM_TYPE, mp4; | 20 | MP2T_PACKET_LENGTH, H264_STREAM_TYPE, ADTS_STREAM_TYPE, |
21 | ADTS_SAMPLING_FREQUENCIES, mp4; | ||
21 | 22 | ||
22 | MP2T_PACKET_LENGTH = 188; // bytes | 23 | MP2T_PACKET_LENGTH = 188; // bytes |
23 | H264_STREAM_TYPE = 0x1b; | 24 | H264_STREAM_TYPE = 0x1b; |
24 | ADTS_STREAM_TYPE = 0x0f; | 25 | ADTS_STREAM_TYPE = 0x0f; |
26 | ADTS_SAMPLING_FREQUENCIES = [ | ||
27 | 96000, | ||
28 | 88200, | ||
29 | 64000, | ||
30 | 48000, | ||
31 | 44100, | ||
32 | 32000, | ||
33 | 24000, | ||
34 | 22050, | ||
35 | 16000, | ||
36 | 12000, | ||
37 | 11025, | ||
38 | 8000, | ||
39 | 7350 | ||
40 | ]; | ||
41 | |||
25 | mp4 = videojs.mp4; | 42 | mp4 = videojs.mp4; |
26 | 43 | ||
27 | /** | 44 | /** |
... | @@ -438,6 +455,11 @@ AacStream = function() { | ... | @@ -438,6 +455,11 @@ AacStream = function() { |
438 | 455 | ||
439 | // deliver the AAC frame | 456 | // deliver the AAC frame |
440 | this.trigger('data', { | 457 | this.trigger('data', { |
458 | channelcount: ((buffer[i + 1] & 1) << 3) | | ||
459 | ((buffer[i + 2] & 0xc0) >> 6), | ||
460 | samplerate: ADTS_SAMPLING_FREQUENCIES[(buffer[i + 1] & 0x3c) >> 2], | ||
461 | // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 | ||
462 | samplesize: 16, | ||
441 | data: buffer.subarray(i + 6, i + frameLength - 1) | 463 | data: buffer.subarray(i + 6, i + frameLength - 1) |
442 | }); | 464 | }); |
443 | 465 | ||
... | @@ -457,6 +479,62 @@ AacStream = function() { | ... | @@ -457,6 +479,62 @@ AacStream = function() { |
457 | AacStream.prototype = new videojs.Hls.Stream(); | 479 | AacStream.prototype = new videojs.Hls.Stream(); |
458 | 480 | ||
459 | /** | 481 | /** |
482 | * Constructs a single-track, ISO BMFF media segment from AAC data | ||
483 | * events. The output of this stream can be fed to a SourceBuffer | ||
484 | * configured with a suitable initialization segment. | ||
485 | */ | ||
486 | // TODO: share common code with VideoSegmentStream | ||
487 | AudioSegmentStream = function(track) { | ||
488 | var aacFrames = [], aacFramesLength = 0, sequenceNumber = 0; | ||
489 | AudioSegmentStream.prototype.init.call(this); | ||
490 | |||
491 | this.push = function(data) { | ||
492 | // buffer audio data until end() is called | ||
493 | aacFrames.push(data); | ||
494 | aacFramesLength += data.data.byteLength; | ||
495 | }; | ||
496 | |||
497 | this.end = function() { | ||
498 | var boxes, currentFrame, data, sample, i, mdat, moof; | ||
499 | // return early if no audio data has been observed | ||
500 | if (aacFramesLength === 0) { | ||
501 | return; | ||
502 | } | ||
503 | |||
504 | // concatenate the audio data to constuct the mdat | ||
505 | data = new Uint8Array(aacFramesLength); | ||
506 | track.samples = []; | ||
507 | while (aacFramesLength.length) { | ||
508 | currentFrame = aacFrames[0]; | ||
509 | sample = { | ||
510 | size: currentFrame.data.byteLength, | ||
511 | duration: 1024 // FIXME calculate for realz | ||
512 | }; | ||
513 | track.samples.push(sample); | ||
514 | |||
515 | data.set(currentFrame.data, i); | ||
516 | i += currentFrame.data.byteLength; | ||
517 | |||
518 | aacFrames.shift(); | ||
519 | } | ||
520 | aacFramesLength = 0; | ||
521 | mdat = mp4.mdat(data); | ||
522 | |||
523 | moof = mp4.moof(sequenceNumber, [track]); | ||
524 | boxes = new Uint8Array(moof.byteLength + mdat.byteLength); | ||
525 | |||
526 | // bump the sequence number for next time | ||
527 | sequenceNumber++; | ||
528 | |||
529 | boxes.set(moof); | ||
530 | boxes.set(mdat, moof.byteLength); | ||
531 | |||
532 | this.trigger('data', boxes); | ||
533 | }; | ||
534 | }; | ||
535 | AudioSegmentStream.prototype = new videojs.Hls.Stream(); | ||
536 | |||
537 | /** | ||
460 | * Accepts a NAL unit byte stream and unpacks the embedded NAL units. | 538 | * Accepts a NAL unit byte stream and unpacks the embedded NAL units. |
461 | */ | 539 | */ |
462 | NalByteStream = function() { | 540 | NalByteStream = function() { |
... | @@ -539,7 +617,7 @@ NalByteStream = function() { | ... | @@ -539,7 +617,7 @@ NalByteStream = function() { |
539 | 617 | ||
540 | this.end = function() { | 618 | this.end = function() { |
541 | // deliver the last buffered NAL unit | 619 | // deliver the last buffered NAL unit |
542 | if (buffer.byteLength > 3) { | 620 | if (buffer && buffer.byteLength > 3) { |
543 | this.trigger('data', buffer.subarray(syncPoint + 3)); | 621 | this.trigger('data', buffer.subarray(syncPoint + 3)); |
544 | } | 622 | } |
545 | }; | 623 | }; |
... | @@ -763,12 +841,19 @@ VideoSegmentStream = function(track) { | ... | @@ -763,12 +841,19 @@ VideoSegmentStream = function(track) { |
763 | this.end = function() { | 841 | this.end = function() { |
764 | var startUnit, currentNal, moof, mdat, boxes, i, data, view, sample; | 842 | var startUnit, currentNal, moof, mdat, boxes, i, data, view, sample; |
765 | 843 | ||
844 | // return early if no video data has been observed | ||
845 | if (nalUnitsLength === 0) { | ||
846 | return; | ||
847 | } | ||
848 | |||
766 | // concatenate the video data and construct the mdat | 849 | // concatenate the video data and construct the mdat |
767 | // first, we have to build the index from byte locations to | 850 | // first, we have to build the index from byte locations to |
768 | // samples (that is, frames) in the video data | 851 | // samples (that is, frames) in the video data |
769 | data = new Uint8Array(nalUnitsLength + (4 * nalUnits.length)); | 852 | data = new Uint8Array(nalUnitsLength + (4 * nalUnits.length)); |
770 | view = new DataView(data.buffer); | 853 | view = new DataView(data.buffer); |
771 | track.samples = []; | 854 | track.samples = []; |
855 | |||
856 | // see ISO/IEC 14496-12:2012, section 8.6.4.3 | ||
772 | sample = { | 857 | sample = { |
773 | size: 0, | 858 | size: 0, |
774 | flags: { | 859 | flags: { |
... | @@ -853,11 +938,14 @@ VideoSegmentStream.prototype = new videojs.Hls.Stream(); | ... | @@ -853,11 +938,14 @@ VideoSegmentStream.prototype = new videojs.Hls.Stream(); |
853 | Transmuxer = function() { | 938 | Transmuxer = function() { |
854 | var | 939 | var |
855 | self = this, | 940 | self = this, |
856 | track, | 941 | videoTrack, |
942 | audioTrack, | ||
857 | config, | 943 | config, |
858 | pps, | 944 | pps, |
859 | 945 | ||
860 | packetStream, parseStream, elementaryStream, aacStream, h264Stream, videoSegmentStream; | 946 | packetStream, parseStream, elementaryStream, |
947 | aacStream, h264Stream, | ||
948 | videoSegmentStream, audioSegmentStream; | ||
861 | 949 | ||
862 | Transmuxer.prototype.init.call(this); | 950 | Transmuxer.prototype.init.call(this); |
863 | 951 | ||
... | @@ -880,51 +968,78 @@ Transmuxer = function() { | ... | @@ -880,51 +968,78 @@ Transmuxer = function() { |
880 | !config) { | 968 | !config) { |
881 | config = data.config; | 969 | config = data.config; |
882 | 970 | ||
883 | track.width = config.width; | 971 | videoTrack.width = config.width; |
884 | track.height = config.height; | 972 | videoTrack.height = config.height; |
885 | track.sps = [data.data]; | 973 | videoTrack.sps = [data.data]; |
886 | track.profileIdc = config.profileIdc; | 974 | videoTrack.profileIdc = config.profileIdc; |
887 | track.levelIdc = config.levelIdc; | 975 | videoTrack.levelIdc = config.levelIdc; |
888 | track.profileCompatibility = config.profileCompatibility; | 976 | videoTrack.profileCompatibility = config.profileCompatibility; |
889 | 977 | ||
890 | // generate an init segment once all the metadata is available | 978 | // generate an init segment once all the metadata is available |
891 | if (pps) { | 979 | if (pps) { |
892 | self.trigger('data', { | 980 | self.trigger('data', { |
893 | data: videojs.mp4.initSegment([track]) | 981 | type: 'video', |
982 | data: videojs.mp4.initSegment([videoTrack]) | ||
894 | }); | 983 | }); |
895 | } | 984 | } |
896 | } | 985 | } |
897 | if (data.nalUnitType === 'pic_parameter_set_rbsp' && | 986 | if (data.nalUnitType === 'pic_parameter_set_rbsp' && |
898 | !pps) { | 987 | !pps) { |
899 | pps = data.data; | 988 | pps = data.data; |
900 | track.pps = [data.data]; | 989 | videoTrack.pps = [data.data]; |
901 | 990 | ||
902 | if (config) { | 991 | if (config) { |
903 | self.trigger('data', { | 992 | self.trigger('data', { |
904 | data: videojs.mp4.initSegment([track]) | 993 | type: 'video', |
994 | data: videojs.mp4.initSegment([videoTrack]) | ||
905 | }); | 995 | }); |
906 | } | 996 | } |
907 | } | 997 | } |
908 | }); | 998 | }); |
909 | // hook up the video segment stream once track metadata is delivered | 999 | // generate an init segment based on the first audio sample |
910 | elementaryStream.on('data', function(data) { | 1000 | aacStream.on('data', function(data) { |
911 | var i, triggerData = function(segment) { | 1001 | if (audioTrack && audioTrack.channelcount === undefined) { |
1002 | audioTrack.channelcount = data.channelcount; | ||
1003 | audioTrack.samplerate = data.samplerate; | ||
1004 | audioTrack.samplesize = data.samplesize; | ||
912 | self.trigger('data', { | 1005 | self.trigger('data', { |
913 | data: segment | 1006 | type: 'audio', |
1007 | data: videojs.mp4.initSegment([audioTrack]) | ||
914 | }); | 1008 | }); |
1009 | } | ||
1010 | }); | ||
1011 | // hook up the segment streams once track metadata is delivered | ||
1012 | elementaryStream.on('data', function(data) { | ||
1013 | var i, triggerData = function(type) { | ||
1014 | return function(segment) { | ||
1015 | self.trigger('data', { | ||
1016 | type: type, | ||
1017 | data: segment | ||
1018 | }); | ||
1019 | }; | ||
915 | }; | 1020 | }; |
916 | if (data.type === 'metadata') { | 1021 | if (data.type === 'metadata') { |
917 | i = data.tracks.length; | 1022 | i = data.tracks.length; |
1023 | |||
1024 | // scan the tracks listed in the metadata | ||
918 | while (i--) { | 1025 | while (i--) { |
919 | if (data.tracks[i].type === 'video') { | 1026 | |
920 | track = data.tracks[i]; | 1027 | // hook up the video segment stream to the first track with h264 data |
921 | if (!videoSegmentStream) { | 1028 | if (data.tracks[i].type === 'video' && !videoSegmentStream) { |
922 | videoSegmentStream = new VideoSegmentStream(track); | 1029 | videoTrack = data.tracks[i]; |
923 | h264Stream.pipe(videoSegmentStream); | 1030 | videoSegmentStream = new VideoSegmentStream(videoTrack); |
924 | videoSegmentStream.on('data', triggerData); | 1031 | h264Stream.pipe(videoSegmentStream); |
925 | } | 1032 | videoSegmentStream.on('data', triggerData('video')); |
926 | break; | 1033 | break; |
927 | } | 1034 | } |
1035 | |||
1036 | // hook up the audio segment stream to the first track with aac data | ||
1037 | if (data.tracks[i].type === 'audio' && !audioSegmentStream) { | ||
1038 | audioTrack = data.tracks[i]; | ||
1039 | audioSegmentStream = new AudioSegmentStream(audioTrack); | ||
1040 | aacStream.pipe(audioSegmentStream); | ||
1041 | audioSegmentStream.on('data', triggerData('audio')); | ||
1042 | } | ||
928 | } | 1043 | } |
929 | } | 1044 | } |
930 | }); | 1045 | }); |
... | @@ -938,6 +1053,7 @@ Transmuxer = function() { | ... | @@ -938,6 +1053,7 @@ Transmuxer = function() { |
938 | elementaryStream.end(); | 1053 | elementaryStream.end(); |
939 | h264Stream.end(); | 1054 | h264Stream.end(); |
940 | videoSegmentStream.end(); | 1055 | videoSegmentStream.end(); |
1056 | audioSegmentStream.end(); | ||
941 | }; | 1057 | }; |
942 | }; | 1058 | }; |
943 | Transmuxer.prototype = new videojs.Hls.Stream(); | 1059 | Transmuxer.prototype = new videojs.Hls.Stream(); | ... | ... |
... | @@ -22,7 +22,11 @@ | ... | @@ -22,7 +22,11 @@ |
22 | */ | 22 | */ |
23 | var | 23 | var |
24 | mp4 = videojs.mp4, | 24 | mp4 = videojs.mp4, |
25 | inspectMp4 = videojs.inspectMp4; | 25 | inspectMp4 = videojs.inspectMp4, |
26 | validateMvhd, validateTrak, validateTkhd, validateMdia, | ||
27 | validateMdhd, validateHdlr, validateMinf, validateDinf, | ||
28 | validateStbl, validateStsd, validateMvex, | ||
29 | validateVideoSample, validateAudioSample; | ||
26 | 30 | ||
27 | module('MP4 Generator'); | 31 | module('MP4 Generator'); |
28 | 32 | ||
... | @@ -39,72 +43,90 @@ test('generates a BSMFF ftyp', function() { | ... | @@ -39,72 +43,90 @@ test('generates a BSMFF ftyp', function() { |
39 | equal(boxes[0].minorVersion, 1, 'minor version is one'); | 43 | equal(boxes[0].minorVersion, 1, 'minor version is one'); |
40 | }); | 44 | }); |
41 | 45 | ||
42 | test('generates a moov', function() { | 46 | validateMvhd = function(mvhd) { |
43 | var boxes, mvhd, tkhd, mdhd, hdlr, minf, mvex, | ||
44 | data = mp4.moov([{ | ||
45 | id: 7, | ||
46 | duration: 100, | ||
47 | width: 600, | ||
48 | height: 300, | ||
49 | type: 'video', | ||
50 | profileIdc: 3, | ||
51 | levelIdc: 5, | ||
52 | profileCompatibility: 7, | ||
53 | sps: [new Uint8Array([0, 1, 2]), new Uint8Array([3, 4, 5])], | ||
54 | pps: [new Uint8Array([6, 7, 8])] | ||
55 | }]); | ||
56 | |||
57 | ok(data, 'box is not null'); | ||
58 | |||
59 | boxes = inspectMp4(data); | ||
60 | equal(boxes.length, 1, 'generated a single box'); | ||
61 | equal(boxes[0].type, 'moov', 'generated a moov type'); | ||
62 | equal(boxes[0].size, data.byteLength, 'generated size'); | ||
63 | equal(boxes[0].boxes.length, 3, 'generated three sub boxes'); | ||
64 | |||
65 | mvhd = boxes[0].boxes[0]; | ||
66 | equal(mvhd.type, 'mvhd', 'generated a mvhd'); | 47 | equal(mvhd.type, 'mvhd', 'generated a mvhd'); |
67 | equal(mvhd.duration, 0xffffffff, 'wrote the maximum movie header duration'); | 48 | equal(mvhd.duration, 0xffffffff, 'wrote the maximum movie header duration'); |
68 | equal(mvhd.nextTrackId, 0xffffffff, 'wrote the max next track id'); | 49 | equal(mvhd.nextTrackId, 0xffffffff, 'wrote the max next track id'); |
50 | }; | ||
69 | 51 | ||
70 | equal(boxes[0].boxes[1].type, 'trak', 'generated a trak'); | 52 | validateTrak = function(trak, expected) { |
71 | equal(boxes[0].boxes[1].boxes.length, 2, 'generated two track sub boxes'); | 53 | expected = expected || {}; |
72 | tkhd = boxes[0].boxes[1].boxes[0]; | 54 | equal(trak.type, 'trak', 'generated a trak'); |
55 | equal(trak.boxes.length, 2, 'generated two track sub boxes'); | ||
56 | |||
57 | validateTkhd(trak.boxes[0], expected); | ||
58 | validateMdia(trak.boxes[1], expected); | ||
59 | }; | ||
60 | |||
61 | validateTkhd = function(tkhd, expected) { | ||
73 | equal(tkhd.type, 'tkhd', 'generated a tkhd'); | 62 | equal(tkhd.type, 'tkhd', 'generated a tkhd'); |
74 | equal(tkhd.trackId, 7, 'wrote the track id'); | 63 | equal(tkhd.trackId, 7, 'wrote the track id'); |
75 | deepEqual(tkhd.flags, new Uint8Array([0, 0, 7]), 'flags should equal 7'); | 64 | deepEqual(tkhd.flags, new Uint8Array([0, 0, 7]), 'flags should equal 7'); |
76 | equal(tkhd.duration, 100, 'wrote duration into the track header'); | 65 | equal(tkhd.duration, |
77 | equal(tkhd.width, 600, 'wrote width into the track header'); | 66 | expected.duration || Math.pow(2, 32) - 1, |
78 | equal(tkhd.height, 300, 'wrote height into the track header'); | 67 | 'wrote duration into the track header'); |
79 | 68 | equal(tkhd.width, expected.width || 0, 'wrote width into the track header'); | |
80 | equal(boxes[0].boxes[1].boxes[1].type, 'mdia', 'generated an mdia type'); | 69 | equal(tkhd.height, expected.height || 0, 'wrote height into the track header'); |
81 | equal(boxes[0].boxes[1].boxes[1].boxes.length, 3, 'generated three track media sub boxes'); | 70 | equal(tkhd.volume, 1, 'set volume to 1'); |
82 | 71 | }; | |
83 | mdhd = boxes[0].boxes[1].boxes[1].boxes[0]; | 72 | |
73 | validateMdia = function(mdia, expected) { | ||
74 | equal(mdia.type, 'mdia', 'generated an mdia type'); | ||
75 | equal(mdia.boxes.length, 3, 'generated three track media sub boxes'); | ||
76 | |||
77 | validateMdhd(mdia.boxes[0], expected); | ||
78 | validateHdlr(mdia.boxes[1], expected); | ||
79 | validateMinf(mdia.boxes[2], expected); | ||
80 | }; | ||
81 | |||
82 | validateMdhd = function(mdhd, expected) { | ||
84 | equal(mdhd.type, 'mdhd', 'generate an mdhd type'); | 83 | equal(mdhd.type, 'mdhd', 'generate an mdhd type'); |
85 | equal(mdhd.language, 'und', 'wrote undetermined language'); | 84 | equal(mdhd.language, 'und', 'wrote undetermined language'); |
86 | equal(mdhd.duration, 100, 'wrote duration into the media header'); | 85 | equal(mdhd.timescale, expected.timescale || 90000, 'wrote the timescale'); |
86 | equal(mdhd.duration, | ||
87 | expected.duration || Math.pow(2, 32) - 1, | ||
88 | 'wrote duration into the media header'); | ||
89 | }; | ||
87 | 90 | ||
88 | hdlr = boxes[0].boxes[1].boxes[1].boxes[1]; | 91 | validateHdlr = function(hdlr, expected) { |
89 | equal(hdlr.type, 'hdlr', 'generate an hdlr type'); | 92 | equal(hdlr.type, 'hdlr', 'generate an hdlr type'); |
90 | equal(hdlr.handlerType, 'vide', 'wrote a video handler'); | 93 | if (expected.type !== 'audio') { |
91 | equal(hdlr.name, 'VideoHandler', 'wrote the handler name'); | 94 | equal(hdlr.handlerType, 'vide', 'wrote a video handler'); |
92 | 95 | equal(hdlr.name, 'VideoHandler', 'wrote the handler name'); | |
93 | minf = boxes[0].boxes[1].boxes[1].boxes[2]; | 96 | } else { |
97 | equal(hdlr.handlerType, 'soun', 'wrote a sound handler'); | ||
98 | equal(hdlr.name, 'SoundHandler', 'wrote the sound handler name'); | ||
99 | } | ||
100 | }; | ||
101 | |||
102 | validateMinf = function(minf, expected) { | ||
94 | equal(minf.type, 'minf', 'generate an minf type'); | 103 | equal(minf.type, 'minf', 'generate an minf type'); |
95 | equal(minf.boxes.length, 3, 'generates three minf sub boxes'); | 104 | equal(minf.boxes.length, 3, 'generates three minf sub boxes'); |
96 | 105 | ||
97 | equal(minf.boxes[0].type, 'vmhd', 'generates a vmhd type'); | 106 | if (expected.type !== 'audio') { |
98 | deepEqual({ | 107 | deepEqual({ |
99 | type: 'vmhd', | 108 | type: 'vmhd', |
100 | size: 20, | 109 | size: 20, |
101 | version: 0, | 110 | version: 0, |
102 | flags: new Uint8Array([0, 0, 1]), | 111 | flags: new Uint8Array([0, 0, 1]), |
103 | graphicsmode: 0, | 112 | graphicsmode: 0, |
104 | opcolor: new Uint16Array([0, 0, 0]) | 113 | opcolor: new Uint16Array([0, 0, 0]) |
105 | }, minf.boxes[0], 'generates a vhmd'); | 114 | }, minf.boxes[0], 'generates a vhmd'); |
115 | } else { | ||
116 | deepEqual({ | ||
117 | type: 'smhd', | ||
118 | size: 16, | ||
119 | version: 0, | ||
120 | flags: new Uint8Array([0, 0, 0]), | ||
121 | balance: 0 | ||
122 | }, minf.boxes[0], 'generates an smhd'); | ||
123 | } | ||
106 | 124 | ||
107 | equal(minf.boxes[1].type, 'dinf', 'generates a dinf type'); | 125 | validateDinf(minf.boxes[1]); |
126 | validateStbl(minf.boxes[2], expected); | ||
127 | }; | ||
128 | |||
129 | validateDinf = function(dinf) { | ||
108 | deepEqual({ | 130 | deepEqual({ |
109 | type: 'dinf', | 131 | type: 'dinf', |
110 | size: 36, | 132 | size: 36, |
... | @@ -120,82 +142,123 @@ test('generates a moov', function() { | ... | @@ -120,82 +142,123 @@ test('generates a moov', function() { |
120 | flags: new Uint8Array([0, 0, 1]) | 142 | flags: new Uint8Array([0, 0, 1]) |
121 | }] | 143 | }] |
122 | }] | 144 | }] |
123 | }, minf.boxes[1], 'generates a dinf'); | 145 | }, dinf, 'generates a dinf'); |
146 | }; | ||
147 | |||
148 | validateStbl = function(stbl, expected) { | ||
149 | equal(stbl.type, 'stbl', 'generates an stbl type'); | ||
150 | equal(stbl.boxes.length, 5, 'generated five stbl child boxes'); | ||
124 | 151 | ||
125 | equal(minf.boxes[2].type, 'stbl', 'generates an stbl type'); | 152 | validateStsd(stbl.boxes[0], expected); |
126 | deepEqual({ | 153 | deepEqual({ |
127 | type: 'stbl', | 154 | type: 'stts', |
128 | size: 228, | 155 | size: 16, |
129 | boxes: [{ | 156 | version: 0, |
130 | type: 'stsd', | 157 | flags: new Uint8Array([0, 0, 0]), |
131 | size: 152, | 158 | timeToSamples: [] |
132 | version: 0, | 159 | }, stbl.boxes[1], 'generated an stts'); |
133 | flags: new Uint8Array([0, 0, 0]), | 160 | deepEqual({ |
134 | sampleDescriptions: [{ | 161 | type: 'stsc', |
135 | type: 'avc1', | 162 | size: 16, |
136 | size: 136, | 163 | version: 0, |
137 | dataReferenceIndex: 1, | 164 | flags: new Uint8Array([0, 0, 0]), |
138 | width: 600, | 165 | sampleToChunks: [] |
139 | height: 300, | 166 | }, stbl.boxes[2], 'generated an stsc'); |
140 | horizresolution: 72, | 167 | deepEqual({ |
141 | vertresolution: 72, | 168 | type: 'stsz', |
142 | frameCount: 1, | 169 | version: 0, |
143 | depth: 24, | 170 | size: 20, |
144 | config: [{ | 171 | flags: new Uint8Array([0, 0, 0]), |
145 | type: 'avcC', | 172 | sampleSize: 0, |
146 | size: 30, | 173 | entries: [] |
147 | configurationVersion: 1, | 174 | }, stbl.boxes[3], 'generated an stsz'); |
148 | avcProfileIndication: 3, | 175 | deepEqual({ |
149 | avcLevelIndication: 5, | 176 | type: 'stco', |
150 | profileCompatibility: 7, | 177 | size: 16, |
151 | lengthSizeMinusOne: 3, | 178 | version: 0, |
152 | sps: [new Uint8Array([ | 179 | flags: new Uint8Array([0, 0, 0]), |
153 | 0, 1, 2 | 180 | chunkOffsets: [] |
154 | ]), new Uint8Array([ | 181 | }, stbl.boxes[4], 'generated and stco'); |
155 | 3, 4, 5 | 182 | }; |
156 | ])], | 183 | |
157 | pps: [new Uint8Array([ | 184 | validateStsd = function(stsd, expected) { |
158 | 6, 7, 8 | 185 | equal(stsd.type, 'stsd', 'generated an stsd'); |
159 | ])] | 186 | equal(stsd.sampleDescriptions.length, 1, 'generated one sample'); |
160 | }, { | 187 | if (expected.type !== 'audio') { |
161 | type: 'btrt', | 188 | validateVideoSample(stsd.sampleDescriptions[0]); |
162 | size: 20, | 189 | } else { |
163 | bufferSizeDB: 1875072, | 190 | validateAudioSample(stsd.sampleDescriptions[0]); |
164 | maxBitrate: 3000000, | 191 | } |
165 | avgBitrate: 3000000 | 192 | }; |
166 | }] | 193 | |
167 | }] | 194 | validateVideoSample = function(sample) { |
168 | }, { | 195 | deepEqual(sample, { |
169 | type: 'stts', | 196 | type: 'avc1', |
170 | size: 16, | 197 | size: 136, |
171 | version: 0, | 198 | dataReferenceIndex: 1, |
172 | flags: new Uint8Array([0, 0, 0]), | 199 | width: 600, |
173 | timeToSamples: [] | 200 | height: 300, |
174 | }, { | 201 | horizresolution: 72, |
175 | type: 'stsc', | 202 | vertresolution: 72, |
176 | size: 16, | 203 | frameCount: 1, |
177 | version: 0, | 204 | depth: 24, |
178 | flags: new Uint8Array([0, 0, 0]), | 205 | config: [{ |
179 | sampleToChunks: [] | 206 | type: 'avcC', |
207 | size: 30, | ||
208 | configurationVersion: 1, | ||
209 | avcProfileIndication: 3, | ||
210 | avcLevelIndication: 5, | ||
211 | profileCompatibility: 7, | ||
212 | lengthSizeMinusOne: 3, | ||
213 | sps: [new Uint8Array([ | ||
214 | 0, 1, 2 | ||
215 | ]), new Uint8Array([ | ||
216 | 3, 4, 5 | ||
217 | ])], | ||
218 | pps: [new Uint8Array([ | ||
219 | 6, 7, 8 | ||
220 | ])] | ||
180 | }, { | 221 | }, { |
181 | type: 'stsz', | 222 | type: 'btrt', |
182 | version: 0, | ||
183 | size: 20, | 223 | size: 20, |
184 | flags: new Uint8Array([0, 0, 0]), | 224 | bufferSizeDB: 1875072, |
185 | sampleSize: 0, | 225 | maxBitrate: 3000000, |
186 | entries: [] | 226 | avgBitrate: 3000000 |
187 | }, { | 227 | }] |
188 | type: 'stco', | 228 | }, 'generated a video sample'); |
189 | size: 16, | 229 | }; |
230 | |||
231 | validateAudioSample = function(sample) { | ||
232 | deepEqual(sample, { | ||
233 | type: 'mp4a', | ||
234 | size: 75, | ||
235 | dataReferenceIndex: 1, | ||
236 | channelcount: 2, | ||
237 | samplesize: 16, | ||
238 | samplerate: 48000, | ||
239 | streamDescriptor: { | ||
240 | type: 'esds', | ||
190 | version: 0, | 241 | version: 0, |
191 | flags: new Uint8Array([0, 0, 0]), | 242 | flags: new Uint8Array([0, 0, 0]), |
192 | chunkOffsets: [] | 243 | size: 39, |
193 | }] | 244 | esId: 0, |
194 | }, minf.boxes[2], 'generates a stbl'); | 245 | streamPriority: 0, |
195 | 246 | // these values were hard-coded based on a working audio init segment | |
196 | 247 | decoderConfig: { | |
197 | mvex = boxes[0].boxes[2]; | 248 | avgBitrate: 56000, |
198 | equal(mvex.type, 'mvex', 'generates an mvex type'); | 249 | maxBitrate: 56000, |
250 | bufferSize: 1536, | ||
251 | objectProfileIndication: 64, | ||
252 | streamType: 5 | ||
253 | } | ||
254 | } | ||
255 | }, 'generated an audio sample'); | ||
256 | }; | ||
257 | |||
258 | validateMvex = function(mvex, options) { | ||
259 | options = options || { | ||
260 | sampleDegradationPriority: 1 | ||
261 | }; | ||
199 | deepEqual({ | 262 | deepEqual({ |
200 | type: 'mvex', | 263 | type: 'mvex', |
201 | size: 40, | 264 | size: 40, |
... | @@ -213,17 +276,75 @@ test('generates a moov', function() { | ... | @@ -213,17 +276,75 @@ test('generates a moov', function() { |
213 | sampleHasRedundancy: 0, | 276 | sampleHasRedundancy: 0, |
214 | samplePaddingValue: 0, | 277 | samplePaddingValue: 0, |
215 | sampleIsDifferenceSample: true, | 278 | sampleIsDifferenceSample: true, |
216 | sampleDegradationPriority: 1 | 279 | sampleDegradationPriority: options.sampleDegradationPriority |
217 | }] | 280 | }] |
218 | }, mvex, 'writes a movie extends box'); | 281 | }, mvex, 'writes a movie extends box'); |
282 | }; | ||
283 | |||
284 | test('generates a video moov', function() { | ||
285 | var | ||
286 | boxes, | ||
287 | data = mp4.moov([{ | ||
288 | id: 7, | ||
289 | duration: 100, | ||
290 | width: 600, | ||
291 | height: 300, | ||
292 | type: 'video', | ||
293 | profileIdc: 3, | ||
294 | levelIdc: 5, | ||
295 | profileCompatibility: 7, | ||
296 | sps: [new Uint8Array([0, 1, 2]), new Uint8Array([3, 4, 5])], | ||
297 | pps: [new Uint8Array([6, 7, 8])] | ||
298 | }]); | ||
299 | |||
300 | ok(data, 'box is not null'); | ||
301 | boxes = inspectMp4(data); | ||
302 | equal(boxes.length, 1, 'generated a single box'); | ||
303 | equal(boxes[0].type, 'moov', 'generated a moov type'); | ||
304 | equal(boxes[0].size, data.byteLength, 'generated size'); | ||
305 | equal(boxes[0].boxes.length, 3, 'generated three sub boxes'); | ||
306 | |||
307 | validateMvhd(boxes[0].boxes[0]); | ||
308 | validateTrak(boxes[0].boxes[1], { | ||
309 | duration: 100, | ||
310 | width: 600, | ||
311 | height: 300 | ||
312 | }); | ||
313 | validateMvex(boxes[0].boxes[2]); | ||
314 | }); | ||
315 | |||
316 | test('generates an audio moov', function() { | ||
317 | var | ||
318 | data = mp4.moov([{ | ||
319 | id: 7, | ||
320 | type: 'audio', | ||
321 | channelcount: 2, | ||
322 | samplerate: 48000, | ||
323 | samplesize: 16 | ||
324 | }]), | ||
325 | boxes; | ||
326 | |||
327 | ok(data, 'box is not null'); | ||
328 | boxes = inspectMp4(data); | ||
329 | equal(boxes.length, 1, 'generated a single box'); | ||
330 | equal(boxes[0].type, 'moov', 'generated a moov type'); | ||
331 | equal(boxes[0].size, data.byteLength, 'generated size'); | ||
332 | equal(boxes[0].boxes.length, 3, 'generated three sub boxes'); | ||
333 | |||
334 | validateMvhd(boxes[0].boxes[0]); | ||
335 | validateTrak(boxes[0].boxes[1], { | ||
336 | type: 'audio', | ||
337 | timescale: 48000 | ||
338 | }); | ||
339 | validateMvex(boxes[0].boxes[2], { | ||
340 | sampleDegradationPriority: 0 | ||
341 | }); | ||
219 | }); | 342 | }); |
220 | 343 | ||
221 | test('generates a sound hdlr', function() { | 344 | test('generates a sound hdlr', function() { |
222 | var boxes, hdlr, | 345 | var boxes, hdlr, |
223 | data = mp4.moov([{ | 346 | data = mp4.moov([{ |
224 | duration:100, | 347 | duration:100, |
225 | width: 600, | ||
226 | height: 300, | ||
227 | type: 'audio' | 348 | type: 'audio' |
228 | }]); | 349 | }]); |
229 | 350 | ... | ... |
... | @@ -586,6 +586,75 @@ test('can parse a video stsd', function() { | ... | @@ -586,6 +586,75 @@ test('can parse a video stsd', function() { |
586 | }]); | 586 | }]); |
587 | }); | 587 | }); |
588 | 588 | ||
589 | test('can parse an audio stsd', function() { | ||
590 | var data = box('stsd', | ||
591 | 0x00, // version 0 | ||
592 | 0x00, 0x00, 0x00, // flags | ||
593 | 0x00, 0x00, 0x00, 0x01, // entry_count | ||
594 | box('mp4a', | ||
595 | 0x00, 0x00, 0x00, | ||
596 | 0x00, 0x00, 0x00, // reserved | ||
597 | 0x00, 0x01, // data_reference_index | ||
598 | 0x00, 0x00, 0x00, 0x00, | ||
599 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
600 | 0x00, 0x02, // channelcount | ||
601 | 0x00, 0x10, // samplesize | ||
602 | 0x00, 0x00, // pre_defined | ||
603 | 0x00, 0x00, // reserved | ||
604 | 0xbb, 0x80, 0x00, 0x00, // samplerate, fixed-point 16.16 | ||
605 | box('esds', | ||
606 | 0x00, // version 0 | ||
607 | 0x00, 0x00, 0x00, // flags | ||
608 | 0x03, // tag, ES_DescrTag | ||
609 | 0x00, // length | ||
610 | 0x00, 0x01, // ES_ID | ||
611 | 0x00, // streamDependenceFlag, URL_Flag, reserved, streamPriority | ||
612 | |||
613 | // DecoderConfigDescriptor | ||
614 | 0x04, // tag, DecoderConfigDescrTag | ||
615 | 0x0d, // length | ||
616 | 0x40, // objectProfileIndication, AAC Main | ||
617 | 0x15, // streamType, AudioStream. upstream, reserved | ||
618 | 0x00, 0x00, 0xff, // bufferSizeDB | ||
619 | 0x00, 0x00, 0x00, 0xff, // maxBitrate | ||
620 | 0x00, 0x00, 0x00, 0xaa, // avgBitrate | ||
621 | |||
622 | // DecoderSpecificInfo | ||
623 | 0x05, // tag, DecoderSpecificInfoTag | ||
624 | 0x02, // length | ||
625 | 0x11, 0x90, 0x06, 0x01, 0x02))); // decoder specific info | ||
626 | |||
627 | deepEqual(videojs.inspectMp4(new Uint8Array(data)), [{ | ||
628 | version: 0, | ||
629 | flags: new Uint8Array([0, 0, 0]), | ||
630 | type: 'stsd', | ||
631 | size: 91, | ||
632 | sampleDescriptions: [{ | ||
633 | type: 'mp4a', | ||
634 | dataReferenceIndex: 1, | ||
635 | channelcount: 2, | ||
636 | samplesize: 16, | ||
637 | samplerate: 48000, | ||
638 | size: 75, | ||
639 | streamDescriptor: { | ||
640 | type: 'esds', | ||
641 | version: 0, | ||
642 | size: 39, | ||
643 | flags: new Uint8Array([0, 0, 0]), | ||
644 | esId: 1, | ||
645 | streamPriority: 0, | ||
646 | decoderConfig: { | ||
647 | objectProfileIndication: 0x40, | ||
648 | streamType: 0x05, | ||
649 | bufferSize: 0xff, | ||
650 | maxBitrate: 0xff, | ||
651 | avgBitrate: 0xaa | ||
652 | } | ||
653 | } | ||
654 | }] | ||
655 | }], 'parsed an audio stsd'); | ||
656 | }); | ||
657 | |||
589 | test('can parse an styp', function() { | 658 | test('can parse an styp', function() { |
590 | deepEqual(videojs.inspectMp4(new Uint8Array(box('styp', | 659 | deepEqual(videojs.inspectMp4(new Uint8Array(box('styp', |
591 | 0x61, 0x76, 0x63, 0x31, // major brand | 660 | 0x61, 0x76, 0x63, 0x31, // major brand |
... | @@ -845,6 +914,24 @@ test('can parse a sidx', function(){ | ... | @@ -845,6 +914,24 @@ test('can parse a sidx', function(){ |
845 | }]); | 914 | }]); |
846 | }); | 915 | }); |
847 | 916 | ||
917 | test('can parse an smhd', function() { | ||
918 | var data = box('smhd', | ||
919 | 0x00, // version | ||
920 | 0x00, 0x00, 0x00, // flags | ||
921 | 0x00, 0xff, // balance, fixed-point 8.8 | ||
922 | 0x00, 0x00); // reserved | ||
923 | |||
924 | deepEqual(videojs.inspectMp4(new Uint8Array(data)), | ||
925 | [{ | ||
926 | type: 'smhd', | ||
927 | size: 16, | ||
928 | version: 0, | ||
929 | flags: new Uint8Array([0, 0, 0]), | ||
930 | balance: 0xff / Math.pow(2, 8) | ||
931 | }], | ||
932 | 'parsed an smhd'); | ||
933 | }); | ||
934 | |||
848 | test('can parse a tfdt', function() { | 935 | test('can parse a tfdt', function() { |
849 | var data = box('tfdt', | 936 | var data = box('tfdt', |
850 | 0x00, // version | 937 | 0x00, // version | ... | ... |
... | @@ -129,6 +129,27 @@ var | ... | @@ -129,6 +129,27 @@ var |
129 | avgBitrate: view.getUint32(8) | 129 | avgBitrate: view.getUint32(8) |
130 | }; | 130 | }; |
131 | }, | 131 | }, |
132 | esds: function(data) { | ||
133 | return { | ||
134 | version: data[0], | ||
135 | flags: new Uint8Array(data.subarray(1, 4)), | ||
136 | esId: (data[6] << 8) | data[7], | ||
137 | streamPriority: data[8] & 0x1f, | ||
138 | decoderConfig: { | ||
139 | objectProfileIndication: data[11], | ||
140 | streamType: (data[12] >>> 2) & 0x3f, | ||
141 | bufferSize: (data[13] << 16) | (data[14] << 8) | data[15], | ||
142 | maxBitrate: (data[16] << 24) | | ||
143 | (data[17] << 16) | | ||
144 | (data[18] << 8) | | ||
145 | data[19], | ||
146 | avgBitrate: (data[20] << 24) | | ||
147 | (data[21] << 16) | | ||
148 | (data[22] << 8) | | ||
149 | data[23] | ||
150 | } | ||
151 | }; | ||
152 | }, | ||
132 | ftyp: function(data) { | 153 | ftyp: function(data) { |
133 | var | 154 | var |
134 | view = new DataView(data.buffer, data.byteOffset, data.byteLength), | 155 | view = new DataView(data.buffer, data.byteOffset, data.byteLength), |
... | @@ -247,6 +268,30 @@ var | ... | @@ -247,6 +268,30 @@ var |
247 | boxes: videojs.inspectMp4(data) | 268 | boxes: videojs.inspectMp4(data) |
248 | }; | 269 | }; |
249 | }, | 270 | }, |
271 | // codingname, not a first-class box type. stsd entries share the | ||
272 | // same format as real boxes so the parsing infrastructure can be | ||
273 | // shared | ||
274 | mp4a: function(data) { | ||
275 | var | ||
276 | view = new DataView(data.buffer, data.byteOffset, data.byteLength), | ||
277 | result = { | ||
278 | // 6 bytes reserved | ||
279 | dataReferenceIndex: view.getUint16(6), | ||
280 | // 4 + 4 bytes reserved | ||
281 | channelcount: view.getUint16(16), | ||
282 | samplesize: view.getUint16(18), | ||
283 | // 2 bytes pre_defined | ||
284 | // 2 bytes reserved | ||
285 | samplerate: view.getUint16(24) + (view.getUint16(26) / 65536) | ||
286 | }; | ||
287 | |||
288 | // if there are more bytes to process, assume this is an ISO/IEC | ||
289 | // 14496-14 MP4AudioSampleEntry and parse the ESDBox | ||
290 | if (data.byteLength > 28) { | ||
291 | result.streamDescriptor = videojs.inspectMp4(data.subarray(28))[0]; | ||
292 | } | ||
293 | return result; | ||
294 | }, | ||
250 | moof: function(data) { | 295 | moof: function(data) { |
251 | return { | 296 | return { |
252 | boxes: videojs.inspectMp4(data) | 297 | boxes: videojs.inspectMp4(data) |
... | @@ -357,6 +402,13 @@ var | ... | @@ -357,6 +402,13 @@ var |
357 | 402 | ||
358 | return result; | 403 | return result; |
359 | }, | 404 | }, |
405 | smhd: function(data) { | ||
406 | return { | ||
407 | version: data[0], | ||
408 | flags: new Uint8Array(data.subarray(1, 4)), | ||
409 | balance: data[4] + (data[5] / 256) | ||
410 | }; | ||
411 | }, | ||
360 | stbl: function(data) { | 412 | stbl: function(data) { |
361 | return { | 413 | return { |
362 | boxes: videojs.inspectMp4(data) | 414 | boxes: videojs.inspectMp4(data) | ... | ... |
... | @@ -181,8 +181,8 @@ | ... | @@ -181,8 +181,8 @@ |
181 | 181 | ||
182 | mediaSource.addEventListener('sourceopen', function() { | 182 | mediaSource.addEventListener('sourceopen', function() { |
183 | var | 183 | var |
184 | buffer = mediaSource.addSourceBuffer('video/mp4;codecs=avc1.4d400d'), | 184 | // buffer = mediaSource.addSourceBuffer('video/mp4;codecs=avc1.4d400d'); |
185 | one = false; | 185 | buffer = mediaSource.addSourceBuffer('audio/mp4;codecs=mp4a.40.2'); |
186 | buffer.addEventListener('updatestart', logevent); | 186 | buffer.addEventListener('updatestart', logevent); |
187 | buffer.addEventListener('updateend', logevent); | 187 | buffer.addEventListener('updateend', logevent); |
188 | buffer.addEventListener('error', logevent); | 188 | buffer.addEventListener('error', logevent); |
... | @@ -211,27 +211,43 @@ | ... | @@ -211,27 +211,43 @@ |
211 | var segment = new Uint8Array(reader.result), | 211 | var segment = new Uint8Array(reader.result), |
212 | transmuxer = new videojs.mp2t.Transmuxer(), | 212 | transmuxer = new videojs.mp2t.Transmuxer(), |
213 | events = [], | 213 | events = [], |
214 | i = 0, | ||
215 | bytesLength = 0, | ||
216 | init = false, | ||
214 | bytes, | 217 | bytes, |
215 | hex = ''; | 218 | hex = ''; |
216 | 219 | ||
217 | transmuxer.on('data', function(data) { | 220 | transmuxer.on('data', function(data) { |
218 | if (data) { | 221 | if (data && data.type === 'audio') { |
219 | events.push(data.data); | 222 | events.push(data.data); |
223 | bytesLength += data.data.byteLength; | ||
224 | |||
225 | // XXX Media Sources Testing | ||
226 | if (!init) { | ||
227 | vjsParsed = videojs.inspectMp4(data.data); | ||
228 | console.log('appended tmuxed output'); | ||
229 | window.vjsSourceBuffer.appendBuffer(data.data); | ||
230 | init = true; | ||
231 | } | ||
220 | } | 232 | } |
221 | }); | 233 | }); |
222 | transmuxer.push(segment); | 234 | transmuxer.push(segment); |
223 | transmuxer.end(); | 235 | transmuxer.end(); |
224 | 236 | ||
225 | bytes = new Uint8Array(events[0].byteLength + events[1].byteLength); | 237 | bytes = new Uint8Array(bytesLength); |
226 | bytes.set(events[0]); | 238 | i = 0; |
227 | bytes.set(events[1], events[0].byteLength); | 239 | while (events.length) { |
240 | bytes.set(events[0], i); | ||
241 | i += events[0].byteLength; | ||
242 | events.shift(); | ||
243 | } | ||
228 | 244 | ||
229 | vjsParsed = videojs.inspectMp4(bytes); | 245 | // vjsParsed = videojs.inspectMp4(bytes); |
230 | console.log('transmuxed', vjsParsed); | 246 | console.log('transmuxed', videojs.inspectMp4(bytes)); |
231 | diffParsed(); | 247 | diffParsed(); |
232 | 248 | ||
233 | // clear old box info | 249 | // clear old box info |
234 | vjsBoxes.innerHTML = stringify(vjsParsed, null, ' '); | 250 | vjsBoxes.innerHTML = stringify(videojs.inspectMp4(bytes), null, ' '); |
235 | 251 | ||
236 | // write out the result | 252 | // write out the result |
237 | hex += '<pre>'; | 253 | hex += '<pre>'; |
... | @@ -263,8 +279,7 @@ | ... | @@ -263,8 +279,7 @@ |
263 | workingOutput.innerHTML = hex; | 279 | workingOutput.innerHTML = hex; |
264 | 280 | ||
265 | // XXX Media Sources Testing | 281 | // XXX Media Sources Testing |
266 | window.vjsSourceBuffer.appendBuffer(bytes); | 282 | // window.vjsSourceBuffer.appendBuffer(bytes); |
267 | console.log('appended bytes'); | ||
268 | }); | 283 | }); |
269 | reader.readAsArrayBuffer(this.files[0]); | 284 | reader.readAsArrayBuffer(this.files[0]); |
270 | }, false); | 285 | }, false); | ... | ... |
... | @@ -76,27 +76,41 @@ | ... | @@ -76,27 +76,41 @@ |
76 | 76 | ||
77 | // setup the media source | 77 | // setup the media source |
78 | mediaSource.addEventListener('sourceopen', function() { | 78 | mediaSource.addEventListener('sourceopen', function() { |
79 | var buffer = mediaSource.addSourceBuffer('video/mp4;codecs=avc1.4d400d'), | 79 | var videoBuffer = mediaSource.addSourceBuffer('video/mp4;codecs=avc1.4d400d'), |
80 | audioBuffer = mediaSource.addSourceBuffer('audio/mp4;codecs=mp4a.40.2'), | ||
80 | transmuxer = new videojs.mp2t.Transmuxer(), | 81 | transmuxer = new videojs.mp2t.Transmuxer(), |
81 | segments = []; | 82 | videoSegments = [], |
83 | audioSegments = []; | ||
82 | 84 | ||
83 | // expose the machinery for debugging | 85 | // expose the machinery for debugging |
84 | window.vjsMediaSource = mediaSource; | 86 | window.vjsMediaSource = mediaSource; |
85 | window.vjsSourceBuffer = buffer; | 87 | window.vjsSourceBuffer = videoBuffer; |
86 | window.vjsVideo = demo; | 88 | window.vjsVideo = demo; |
87 | 89 | ||
88 | // transmux the MPEG-TS data to BMFF segments | 90 | // transmux the MPEG-TS data to BMFF segments |
89 | transmuxer.on('data', function(segment) { | 91 | transmuxer.on('data', function(segment) { |
90 | segments.push(segment); | 92 | if (segment.type === 'video') { |
93 | videoSegments.push(segment); | ||
94 | } else { | ||
95 | audioSegments.push(segment); | ||
96 | } | ||
91 | }); | 97 | }); |
92 | transmuxer.push(hazeVideo); | 98 | transmuxer.push(hazeVideo); |
93 | transmuxer.end(); | 99 | transmuxer.end(); |
94 | 100 | ||
95 | // buffer up the video data | 101 | // buffer up the video data |
96 | buffer.appendBuffer(segments.shift().data); | 102 | videoBuffer.appendBuffer(videoSegments.shift().data); |
97 | buffer.addEventListener('updateend', function() { | 103 | videoBuffer.addEventListener('updateend', function() { |
98 | if (segments.length) { | 104 | if (videoSegments.length) { |
99 | buffer.appendBuffer(segments.shift().data); | 105 | videoBuffer.appendBuffer(videoSegments.shift().data); |
106 | } | ||
107 | }); | ||
108 | |||
109 | // buffer up the audio data | ||
110 | audioBuffer.appendBuffer(audioSegments.shift().data); | ||
111 | audioBuffer.addEventListener('updateend', function() { | ||
112 | if (audioSegments.length) { | ||
113 | audioBuffer.appendBuffer(audioSegments.shift().data); | ||
100 | } | 114 | } |
101 | }); | 115 | }); |
102 | }); | 116 | }); | ... | ... |
... | @@ -94,8 +94,10 @@ | ... | @@ -94,8 +94,10 @@ |
94 | var onMediaSourceOpen = function() { | 94 | var onMediaSourceOpen = function() { |
95 | console.log('on media open'); | 95 | console.log('on media open'); |
96 | ms.removeEventListener('sourceopen', onMediaSourceOpen); | 96 | ms.removeEventListener('sourceopen', onMediaSourceOpen); |
97 | var sourceBuffer = ms.addSourceBuffer('video/mp4;codecs="avc1.4D400D"'); | 97 | var videoBuffer = ms.addSourceBuffer('video/mp4;codecs="avc1.4D400D"'); |
98 | sourceBuffer.appendBuffer(bytes); | 98 | videoBuffer.appendBuffer(bytes); |
99 | |||
100 | var audioBuffer = ms.addSourceBuffer('audio/mp4;codecs=mp4a.40.2'); | ||
99 | }; | 101 | }; |
100 | 102 | ||
101 | ms.addEventListener('sourceopen', onMediaSourceOpen); | 103 | ms.addEventListener('sourceopen', onMediaSourceOpen); | ... | ... |
... | @@ -47,7 +47,9 @@ var | ... | @@ -47,7 +47,9 @@ var |
47 | validateTrack, | 47 | validateTrack, |
48 | validateTrackFragment, | 48 | validateTrackFragment, |
49 | 49 | ||
50 | videoPes; | 50 | transportPacket, |
51 | videoPes, | ||
52 | audioPes; | ||
51 | 53 | ||
52 | module('MP2T Packet Stream', { | 54 | module('MP2T Packet Stream', { |
53 | setup: function() { | 55 | setup: function() { |
... | @@ -397,15 +399,22 @@ test('parses an elementary stream packet with a pts and dts', function() { | ... | @@ -397,15 +399,22 @@ test('parses an elementary stream packet with a pts and dts', function() { |
397 | equal(2 / 90, packet.dts, 'parsed the dts'); | 399 | equal(2 / 90, packet.dts, 'parsed the dts'); |
398 | }); | 400 | }); |
399 | 401 | ||
400 | // helper function to create video PES packets | 402 | /** |
401 | videoPes = function(data, first) { | 403 | * Helper function to create transport stream PES packets |
404 | * @param pid {uint8} - the program identifier (PID) | ||
405 | * @param data {arraylike} - the payload bytes | ||
406 | * @payload first {boolean} - true if this PES should be a payload | ||
407 | * unit start | ||
408 | */ | ||
409 | transportPacket = function(pid, data, first) { | ||
402 | var | 410 | var |
403 | adaptationFieldLength = 188 - data.length - (first ? 18 : 17), | 411 | adaptationFieldLength = 188 - data.length - (first ? 15 : 14), |
412 | // transport_packet(), Rec. ITU-T H.222.0, Table 2-2 | ||
404 | result = [ | 413 | result = [ |
405 | // sync byte | 414 | // sync byte |
406 | 0x47, | 415 | 0x47, |
407 | // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001 | 416 | // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001 |
408 | 0x40, 0x11, | 417 | 0x40, pid, |
409 | // tsc:01 afc:11 cc:0000 | 418 | // tsc:01 afc:11 cc:0000 |
410 | 0x70 | 419 | 0x70 |
411 | ].concat([ | 420 | ].concat([ |
... | @@ -422,6 +431,7 @@ videoPes = function(data, first) { | ... | @@ -422,6 +431,7 @@ videoPes = function(data, first) { |
422 | result.push(0xff); | 431 | result.push(0xff); |
423 | } | 432 | } |
424 | 433 | ||
434 | // PES_packet(), Rec. ITU-T H.222.0, Table 2-21 | ||
425 | result = result.concat([ | 435 | result = result.concat([ |
426 | // pscp:0000 0000 0000 0000 0000 0001 | 436 | // pscp:0000 0000 0000 0000 0000 0001 |
427 | 0x00, 0x00, 0x01, | 437 | 0x00, 0x00, 0x01, |
... | @@ -437,14 +447,41 @@ videoPes = function(data, first) { | ... | @@ -437,14 +447,41 @@ videoPes = function(data, first) { |
437 | if (first) { | 447 | if (first) { |
438 | result.push(0x00); | 448 | result.push(0x00); |
439 | } | 449 | } |
440 | result = result.concat([ | 450 | return result.concat(data); |
451 | }; | ||
452 | |||
453 | /** | ||
454 | * Helper function to create video PES packets | ||
455 | * @param data {arraylike} - the payload bytes | ||
456 | * @payload first {boolean} - true if this PES should be a payload | ||
457 | * unit start | ||
458 | */ | ||
459 | videoPes = function(data, first) { | ||
460 | return transportPacket(0x11, [ | ||
441 | // NAL unit start code | 461 | // NAL unit start code |
442 | 0x00, 0x00, 0x01 | 462 | 0x00, 0x00, 0x01 |
443 | ].concat(data)); | 463 | ].concat(data), first); |
444 | return result; | ||
445 | }; | 464 | }; |
446 | standalonePes = videoPes([0xaf, 0x01], true); | 465 | standalonePes = videoPes([0xaf, 0x01], true); |
447 | 466 | ||
467 | /** | ||
468 | * Helper function to create audio PES packets | ||
469 | * @param data {arraylike} - the payload bytes | ||
470 | * @payload first {boolean} - true if this PES should be a payload | ||
471 | * unit start | ||
472 | */ | ||
473 | audioPes = function(data, first) { | ||
474 | var frameLength = data.length + 7; | ||
475 | return transportPacket(0x12, [ | ||
476 | 0xff, 0xf1, // no CRC | ||
477 | 0x10, // AAC Main, 44.1KHz | ||
478 | 0xb0 | ((frameLength & 0x1800) >> 11), // 2 channels | ||
479 | (frameLength & 0x7f8) >> 3, | ||
480 | ((frameLength & 0x07) << 5) + 7, // frame length in bytes | ||
481 | 0x00 // one AAC per ADTS frame | ||
482 | ].concat(data), first); | ||
483 | }; | ||
484 | |||
448 | test('parses an elementary stream packet without a pts or dts', function() { | 485 | test('parses an elementary stream packet without a pts or dts', function() { |
449 | 486 | ||
450 | var packet; | 487 | var packet; |
... | @@ -950,17 +987,24 @@ test('generates AAC frame events from ADTS bytes', function() { | ... | @@ -950,17 +987,24 @@ test('generates AAC frame events from ADTS bytes', function() { |
950 | aacStream.push({ | 987 | aacStream.push({ |
951 | type: 'audio', | 988 | type: 'audio', |
952 | data: new Uint8Array([ | 989 | data: new Uint8Array([ |
953 | 0xff, 0xf1, // no CRC | 990 | 0xff, 0xf1, // no CRC |
954 | 0x00, // AAC Main, 44.1KHz | 991 | 0x10, // AAC Main, 44.1KHz |
955 | 0xfc, 0x01, 0x20, // frame length 9 bytes | 992 | 0xbc, 0x01, 0x20, // 2 channels, frame length 9 bytes |
956 | 0x00, // one AAC per ADTS frame | 993 | 0x00, // one AAC per ADTS frame |
957 | 0x12, 0x34, // AAC payload | 994 | 0x12, 0x34, // AAC payload |
958 | 0x56, 0x78 // extra junk that should be ignored | 995 | 0x56, 0x78 // extra junk that should be ignored |
959 | ]) | 996 | ]) |
960 | }); | 997 | }); |
961 | 998 | ||
962 | equal(frames.length, 1, 'generated one frame'); | 999 | equal(frames.length, 1, 'generated one frame'); |
963 | deepEqual(frames[0].data, new Uint8Array([0x12, 0x34]), 'extracted AAC frame'); | 1000 | deepEqual(frames[0].data, new Uint8Array([0x12, 0x34]), 'extracted AAC frame'); |
1001 | equal(frames[0].channelcount, 2, 'parsed channelcount'); | ||
1002 | equal(frames[0].samplerate, 44100, 'parsed samplerate'); | ||
1003 | |||
1004 | // Chrome only supports 8, 16, and 32 bit sample sizes. Assuming the | ||
1005 | // default value of 16 in ISO/IEC 14496-12 AudioSampleEntry is | ||
1006 | // acceptable. | ||
1007 | equal(frames[0].samplesize, 16, 'parsed samplesize'); | ||
964 | }); | 1008 | }); |
965 | 1009 | ||
966 | // not handled: ADTS with CRC | 1010 | // not handled: ADTS with CRC |
... | @@ -972,7 +1016,7 @@ module('Transmuxer', { | ... | @@ -972,7 +1016,7 @@ module('Transmuxer', { |
972 | } | 1016 | } |
973 | }); | 1017 | }); |
974 | 1018 | ||
975 | test('generates an init segment', function() { | 1019 | test('generates a video init segment', function() { |
976 | var segments = []; | 1020 | var segments = []; |
977 | transmuxer.on('data', function(segment) { | 1021 | transmuxer.on('data', function(segment) { |
978 | segments.push(segment); | 1022 | segments.push(segment); |
... | @@ -980,16 +1024,38 @@ test('generates an init segment', function() { | ... | @@ -980,16 +1024,38 @@ test('generates an init segment', function() { |
980 | transmuxer.push(packetize(PAT)); | 1024 | transmuxer.push(packetize(PAT)); |
981 | transmuxer.push(packetize(PMT)); | 1025 | transmuxer.push(packetize(PMT)); |
982 | transmuxer.push(packetize(videoPes([ | 1026 | transmuxer.push(packetize(videoPes([ |
983 | 0x07, | 1027 | 0x08, 0x01 // pic_parameter_set_rbsp |
1028 | ], true))); | ||
1029 | transmuxer.push(packetize(videoPes([ | ||
1030 | 0x07, // seq_parameter_set_rbsp | ||
984 | 0x27, 0x42, 0xe0, 0x0b, | 1031 | 0x27, 0x42, 0xe0, 0x0b, |
985 | 0xa9, 0x18, 0x60, 0x9d, | 1032 | 0xa9, 0x18, 0x60, 0x9d, |
986 | 0x80, 0x53, 0x06, 0x01, | 1033 | 0x80, 0x53, 0x06, 0x01, |
987 | 0x06, 0xb6, 0xc2, 0xb5, | 1034 | 0x06, 0xb6, 0xc2, 0xb5, |
988 | 0xef, 0x7c, 0x04 | 1035 | 0xef, 0x7c, 0x04 |
1036 | ], false))); | ||
1037 | transmuxer.end(); | ||
1038 | |||
1039 | equal(segments.length, 2, 'generated init and media segments'); | ||
1040 | ok(segments[0].data, 'wrote data in the init segment'); | ||
1041 | equal(segments[0].type, 'video', 'video is the segment type'); | ||
1042 | }); | ||
1043 | |||
1044 | test('generates an audio init segment', function() { | ||
1045 | var segments = []; | ||
1046 | transmuxer.on('data', function(segment) { | ||
1047 | segments.push(segment); | ||
1048 | }); | ||
1049 | transmuxer.push(packetize(PAT)); | ||
1050 | transmuxer.push(packetize(PMT)); | ||
1051 | transmuxer.push(packetize(audioPes([ | ||
1052 | 0x00, 0x01 | ||
989 | ], true))); | 1053 | ], true))); |
990 | transmuxer.end(); | 1054 | transmuxer.end(); |
991 | 1055 | ||
992 | equal(segments.length, 1, 'has an init segment'); | 1056 | equal(segments.length, 2, 'generated init and media segments'); |
1057 | ok(segments[0].data, 'wrote data in the init segment'); | ||
1058 | equal(segments[0].type, 'audio', 'audio is the segment type'); | ||
993 | }); | 1059 | }); |
994 | 1060 | ||
995 | test('buffers video samples until ended', function() { | 1061 | test('buffers video samples until ended', function() { |
... | @@ -1123,20 +1189,26 @@ validateTrackFragment = function(track, segment, metadata) { | ... | @@ -1123,20 +1189,26 @@ validateTrackFragment = function(track, segment, metadata) { |
1123 | 1189 | ||
1124 | test('parses an example mp2t file and generates media segments', function() { | 1190 | test('parses an example mp2t file and generates media segments', function() { |
1125 | var | 1191 | var |
1126 | segments = [], | 1192 | videoSegments = [], |
1193 | audioSegments = [], | ||
1127 | sequenceNumber = window.Infinity, | 1194 | sequenceNumber = window.Infinity, |
1128 | i, boxes, mfhd; | 1195 | i, boxes, mfhd; |
1129 | 1196 | ||
1130 | transmuxer.on('data', function(segment) { | 1197 | transmuxer.on('data', function(segment) { |
1131 | segments.push(segment); | 1198 | if (segment.type === 'video') { |
1199 | videoSegments.push(segment); | ||
1200 | } else if (segment.type === 'audio') { | ||
1201 | audioSegments.push(segment); | ||
1202 | } | ||
1132 | }); | 1203 | }); |
1133 | transmuxer.push(window.bcSegment); | 1204 | transmuxer.push(window.bcSegment); |
1134 | transmuxer.end(); | 1205 | transmuxer.end(); |
1135 | 1206 | ||
1136 | equal(segments.length, 2, 'generated two segments'); | 1207 | equal(videoSegments.length, 2, 'generated two video segments'); |
1208 | equal(audioSegments.length, 2, 'generated two audio segments'); | ||
1137 | 1209 | ||
1138 | boxes = videojs.inspectMp4(segments[0].data); | 1210 | boxes = videojs.inspectMp4(videoSegments[0].data); |
1139 | equal(boxes.length, 2, 'init segments are composed of two boxes'); | 1211 | equal(boxes.length, 2, 'video init segments are composed of two boxes'); |
1140 | equal(boxes[0].type, 'ftyp', 'the first box is an ftyp'); | 1212 | equal(boxes[0].type, 'ftyp', 'the first box is an ftyp'); |
1141 | equal(boxes[1].type, 'moov', 'the second box is a moov'); | 1213 | equal(boxes[1].type, 'moov', 'the second box is a moov'); |
1142 | equal(boxes[1].boxes[0].type, 'mvhd', 'generated an mvhd'); | 1214 | equal(boxes[1].boxes[0].type, 'mvhd', 'generated an mvhd'); |
... | @@ -1150,9 +1222,9 @@ test('parses an example mp2t file and generates media segments', function() { | ... | @@ -1150,9 +1222,9 @@ test('parses an example mp2t file and generates media segments', function() { |
1150 | // }); | 1222 | // }); |
1151 | // equal(boxes[1].boxes[3].type, 'mvex', 'generated an mvex'); | 1223 | // equal(boxes[1].boxes[3].type, 'mvex', 'generated an mvex'); |
1152 | 1224 | ||
1153 | boxes = videojs.inspectMp4(segments[1].data); | 1225 | boxes = videojs.inspectMp4(videoSegments[1].data); |
1154 | ok(boxes.length > 0, 'media segments are not empty'); | 1226 | ok(boxes.length > 0, 'video media segments are not empty'); |
1155 | ok(boxes.length % 2 === 0, 'media segments are composed of pairs of boxes'); | 1227 | ok(boxes.length % 2 === 0, 'video media segments are composed of pairs of boxes'); |
1156 | for (i = 0; i < boxes.length; i += 2) { | 1228 | for (i = 0; i < boxes.length; i += 2) { |
1157 | equal(boxes[i].type, 'moof', 'first box is a moof'); | 1229 | equal(boxes[i].type, 'moof', 'first box is a moof'); |
1158 | equal(boxes[i].boxes.length, 2, 'the moof has two children'); | 1230 | equal(boxes[i].boxes.length, 2, 'the moof has two children'); |
... | @@ -1163,7 +1235,7 @@ test('parses an example mp2t file and generates media segments', function() { | ... | @@ -1163,7 +1235,7 @@ test('parses an example mp2t file and generates media segments', function() { |
1163 | sequenceNumber = mfhd.sequenceNumber; | 1235 | sequenceNumber = mfhd.sequenceNumber; |
1164 | 1236 | ||
1165 | equal(boxes[i + 1].type, 'mdat', 'second box is an mdat'); | 1237 | equal(boxes[i + 1].type, 'mdat', 'second box is an mdat'); |
1166 | validateTrackFragment(boxes[i].boxes[1], segments[1].data, { | 1238 | validateTrackFragment(boxes[i].boxes[1], videoSegments[1].data, { |
1167 | trackId: 256, | 1239 | trackId: 256, |
1168 | width: 388, | 1240 | width: 388, |
1169 | height: 300, | 1241 | height: 300, | ... | ... |
-
Please register or sign in to post a comment