Create mp4 generator
Build helper classes to construct boxes for mp4s. Use the mp4 inspector to validate the generated boxes are sensical. Remove the stubbed Transmuxer because it's not implemented yet and it may not make sense once things come together a bit more. Left some MediaSource test code in mp4.html for debugging purposes. The tests all pass but we're still not quite generating a valid init segment.
Showing
10 changed files
with
632 additions
and
48 deletions
src/mp4-generator.js
0 → 100644
1 | (function(window, videojs, undefined) { | ||
2 | 'use strict'; | ||
3 | |||
4 | var box, dinf, ftyp, minf, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, stbl, | ||
5 | stsd, types, MAJOR_BRAND, MINOR_VERSION, VIDEO_HDLR, DREF, TREX, Uint8Array, DataView; | ||
6 | |||
7 | Uint8Array = window.Uint8Array; | ||
8 | DataView = window.DataView; | ||
9 | |||
10 | // pre-calculate constants | ||
11 | (function() { | ||
12 | var i; | ||
13 | types = { | ||
14 | avc1: [], // codingname | ||
15 | dinf: [], | ||
16 | dref: [], | ||
17 | ftyp: [], | ||
18 | hdlr: [], | ||
19 | mdhd: [], | ||
20 | mdia: [], | ||
21 | minf: [], | ||
22 | moov: [], | ||
23 | mvex: [], | ||
24 | mvhd: [], | ||
25 | stbl: [], | ||
26 | stco: [], | ||
27 | stsc: [], | ||
28 | stsd: [], | ||
29 | stts: [], | ||
30 | trak: [], | ||
31 | trex: [], | ||
32 | tkhd: [] | ||
33 | }; | ||
34 | |||
35 | for (i in types) { | ||
36 | if (types.hasOwnProperty(i)) { | ||
37 | types[i] = [ | ||
38 | i.charCodeAt(0), | ||
39 | i.charCodeAt(1), | ||
40 | i.charCodeAt(2), | ||
41 | i.charCodeAt(3) | ||
42 | ]; | ||
43 | } | ||
44 | } | ||
45 | |||
46 | MAJOR_BRAND = new Uint8Array([ | ||
47 | 'i'.charCodeAt(0), | ||
48 | 's'.charCodeAt(0), | ||
49 | 'o'.charCodeAt(0), | ||
50 | 'm'.charCodeAt(0) | ||
51 | ]); | ||
52 | MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); | ||
53 | VIDEO_HDLR = new Uint8Array([ | ||
54 | 0x00, // version 0 | ||
55 | 0x00, 0x00, 0x00, // flags | ||
56 | 0x00, 0x00, 0x00, 0x00, // pre_defined | ||
57 | 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' | ||
58 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
59 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
60 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
61 | 0x56, 0x69, 0x64, 0x65, | ||
62 | 0x6f, 0x48, 0x61, 0x6e, | ||
63 | 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' | ||
64 | ]); | ||
65 | DREF = new Uint8Array([ | ||
66 | 0x00, // version 0 | ||
67 | 0x00, 0x00, 0x00, // flags | ||
68 | 0x00, 0x00, 0x00, 0x00 // entry_count | ||
69 | ]); | ||
70 | TREX = new Uint8Array([ | ||
71 | 0x00, // version 0 | ||
72 | 0x00, 0x00, 0x00, // flags | ||
73 | 0x00, 0x00, 0x00, 0x01, // track_ID | ||
74 | 0x00, 0x00, 0x00, 0x01, // default_sample_description_index | ||
75 | 0x00, 0x00, 0x00, 0x00, // default_sample_duration | ||
76 | 0x00, 0x00, 0x00, 0x00, // default_sample_size | ||
77 | 0x00, 0x01, 0x00, 0x01 // default_sample_flags | ||
78 | ]); | ||
79 | })(); | ||
80 | |||
81 | box = function(type) { | ||
82 | var | ||
83 | payload = Array.prototype.slice.call(arguments, 1), | ||
84 | size = 0, | ||
85 | i = payload.length, | ||
86 | result, | ||
87 | view; | ||
88 | |||
89 | // calculate the total size we need to allocate | ||
90 | while (i--) { | ||
91 | size += payload[i].byteLength; | ||
92 | } | ||
93 | result = new Uint8Array(size + 8); | ||
94 | view = new DataView(result.buffer, result.byteOffset, result.byteLength); | ||
95 | view.setUint32(0, result.byteLength); | ||
96 | result.set(type, 4); | ||
97 | |||
98 | // copy the payload into the result | ||
99 | for (i = 0, size = 8; i < payload.length; i++) { | ||
100 | result.set(payload[i], size); | ||
101 | size += payload[i].byteLength; | ||
102 | } | ||
103 | return result; | ||
104 | }; | ||
105 | |||
106 | dinf = function() { | ||
107 | return box(types.dinf, box(types.dref, DREF)); | ||
108 | }; | ||
109 | |||
110 | ftyp = function() { | ||
111 | return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND); | ||
112 | }; | ||
113 | |||
114 | hdlr = function() { | ||
115 | return box(types.hdlr, VIDEO_HDLR); | ||
116 | }; | ||
117 | mdhd = function(duration) { | ||
118 | return box(types.mdhd, new Uint8Array([ | ||
119 | 0x00, // version 0 | ||
120 | 0x00, 0x00, 0x00, // flags | ||
121 | 0x00, 0x00, 0x00, 0x02, // creation_time | ||
122 | 0x00, 0x00, 0x00, 0x03, // modification_time | ||
123 | 0x00, 0x00, 0x00, 0x3c, // timescale | ||
124 | (duration & 0xFF000000) >> 24, | ||
125 | (duration & 0xFF0000) >> 16, | ||
126 | (duration & 0xFF00) >> 8, | ||
127 | duration & 0xFF, // duration | ||
128 | 0x55, 0xc4, // 'und' language (undetermined) | ||
129 | 0x00, 0x00 | ||
130 | ])); | ||
131 | }; | ||
132 | mdia = function(duration, width, height) { | ||
133 | return box(types.mdia, mdhd(duration), hdlr(), minf(width, height)); | ||
134 | }; | ||
135 | minf = function(width, height) { | ||
136 | return box(types.minf, dinf(), stbl(width, height)); | ||
137 | }; | ||
138 | moov = function(duration, width, height) { | ||
139 | return box(types.moov, mvhd(duration), trak(duration, width, height), mvex()); | ||
140 | }; | ||
141 | mvex = function() { | ||
142 | return box(types.mvex, box(types.trex, TREX)); | ||
143 | }; | ||
144 | mvhd = function(duration) { | ||
145 | var | ||
146 | bytes = new Uint8Array([ | ||
147 | 0x00, // version 0 | ||
148 | 0x00, 0x00, 0x00, // flags | ||
149 | 0x00, 0x00, 0x00, 0x01, // creation_time | ||
150 | 0x00, 0x00, 0x00, 0x02, // modification_time | ||
151 | 0x00, 0x00, 0x00, 0x01, // timescale, 1 "tick" per second | ||
152 | (duration & 0xFF000000) >> 24, | ||
153 | (duration & 0xFF0000) >> 16, | ||
154 | (duration & 0xFF00) >> 8, | ||
155 | duration & 0xFF, // duration | ||
156 | 0x00, 0x01, 0x00, 0x00, // 1.0 rate | ||
157 | 0x01, 0x00, // 1.0 volume | ||
158 | 0x00, 0x00, // reserved | ||
159 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
160 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
161 | 0x00, 0x01, 0x00, 0x00, | ||
162 | 0x00, 0x00, 0x00, 0x00, | ||
163 | 0x00, 0x00, 0x00, 0x00, | ||
164 | 0x00, 0x00, 0x00, 0x00, | ||
165 | 0x00, 0x01, 0x00, 0x00, | ||
166 | 0x00, 0x00, 0x00, 0x00, | ||
167 | 0x00, 0x00, 0x00, 0x00, | ||
168 | 0x00, 0x00, 0x00, 0x00, | ||
169 | 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix | ||
170 | 0x00, 0x00, 0x00, 0x00, | ||
171 | 0x00, 0x00, 0x00, 0x00, | ||
172 | 0x00, 0x00, 0x00, 0x00, | ||
173 | 0x00, 0x00, 0x00, 0x00, | ||
174 | 0x00, 0x00, 0x00, 0x00, | ||
175 | 0x00, 0x00, 0x00, 0x00, // pre_defined | ||
176 | 0x00, 0x00, 0x00, 0x01 // next_track_ID | ||
177 | ]); | ||
178 | return box(types.mvhd, bytes); | ||
179 | }; | ||
180 | |||
181 | stbl = function(width, height) { | ||
182 | return box(types.stbl, | ||
183 | stsd(width, height), | ||
184 | box(types.stts), | ||
185 | box(types.stsc), | ||
186 | box(types.stco)); | ||
187 | }; | ||
188 | |||
189 | stsd = function(width, height) { | ||
190 | return box(types.stsd, new Uint8Array([ | ||
191 | 0x00, // version 0 | ||
192 | 0x00, 0x00, 0x00, // flags | ||
193 | 0x00, 0x00, 0x00, 0x01]), | ||
194 | box(types.avc1, new Uint8Array([ | ||
195 | 0x00, 0x00, 0x00, | ||
196 | 0x00, 0x00, 0x00, // reserved | ||
197 | 0x00, 0x01, // data_reference_index | ||
198 | 0x00, 0x00, // pre_defined | ||
199 | 0x00, 0x00, // reserved | ||
200 | 0x00, 0x00, 0x00, 0x00, | ||
201 | 0x00, 0x00, 0x00, 0x00, | ||
202 | 0x00, 0x00, 0x00, 0x00, // pre_defined | ||
203 | (width & 0xff00) >> 8, | ||
204 | width & 0xff, // width | ||
205 | (height & 0xff00) >> 8, | ||
206 | height & 0xff, // height | ||
207 | 0x00, 0x48, 0x00, 0x00, // horizresolution | ||
208 | 0x00, 0x48, 0x00, 0x00, // vertresolution | ||
209 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
210 | 0x00, 0x01, // frame_count | ||
211 | 0x13, | ||
212 | 0x76, 0x69, 0x64, 0x65, | ||
213 | 0x6f, 0x6a, 0x73, 0x2d, | ||
214 | 0x63, 0x6f, 0x6e, 0x74, | ||
215 | 0x72, 0x69, 0x62, 0x2d, | ||
216 | 0x68, 0x6c, 0x73, 0x00, | ||
217 | 0x00, 0x00, 0x00, 0x00, | ||
218 | 0x00, 0x00, 0x00, 0x00, | ||
219 | 0x00, 0x00, 0x00, // compressorname | ||
220 | 0x00, 0x18, // depth = 24 | ||
221 | 0x11, 0x11]))); // pre_defined = -1 | ||
222 | }; | ||
223 | |||
224 | tkhd = function(duration, width, height) { | ||
225 | return box(types.tkhd, new Uint8Array([ | ||
226 | 0x00, // version 0 | ||
227 | 0x00, 0x00, 0x00, // flags | ||
228 | 0x00, 0x00, 0x00, 0x00, // creation_time | ||
229 | 0x00, 0x00, 0x00, 0x00, // modification_time | ||
230 | 0x00, 0x00, 0x00, 0x01, // track_ID | ||
231 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
232 | (duration & 0xFF000000) >> 24, | ||
233 | (duration & 0xFF0000) >> 16, | ||
234 | (duration & 0xFF00) >> 8, | ||
235 | duration & 0xFF, // duration | ||
236 | 0x00, 0x00, 0x00, 0x00, | ||
237 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
238 | 0x00, 0x00, // layer | ||
239 | 0x00, 0x00, // alternate_group | ||
240 | 0x00, 0x00, // non-audio track volume | ||
241 | 0x00, 0x00, // reserved | ||
242 | 0x00, 0x01, 0x00, 0x00, | ||
243 | 0x00, 0x00, 0x00, 0x00, | ||
244 | 0x00, 0x00, 0x00, 0x00, | ||
245 | 0x00, 0x00, 0x00, 0x00, | ||
246 | 0x00, 0x01, 0x00, 0x00, | ||
247 | 0x00, 0x00, 0x00, 0x00, | ||
248 | 0x00, 0x00, 0x00, 0x00, | ||
249 | 0x00, 0x00, 0x00, 0x00, | ||
250 | 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix | ||
251 | (width & 0xFF000000) >> 24, | ||
252 | (width & 0xFF0000) >> 16, | ||
253 | (width & 0xFF00) >> 8, | ||
254 | width & 0xFF, // width | ||
255 | (height & 0xFF000000) >> 24, | ||
256 | (height & 0xFF0000) >> 16, | ||
257 | (height & 0xFF00) >> 8, | ||
258 | height & 0xFF // height | ||
259 | ])); | ||
260 | }; | ||
261 | |||
262 | trak = function(duration, width, height) { | ||
263 | return box(types.trak, tkhd(duration, width, height), mdia(duration, width, height)); | ||
264 | }; | ||
265 | |||
266 | window.videojs.mp4 = { | ||
267 | ftyp: ftyp, | ||
268 | moov: moov, | ||
269 | initSegment: function() { | ||
270 | var | ||
271 | fileType = ftyp(), | ||
272 | movie = moov(1, 1280, 720), | ||
273 | result = new Uint8Array(fileType.byteLength + movie.byteLength); | ||
274 | |||
275 | result.set(fileType); | ||
276 | result.set(movie, fileType.byteLength); | ||
277 | return result; | ||
278 | } | ||
279 | }; | ||
280 | |||
281 | })(window, window.videojs); |
1 | /** | 1 | /** |
2 | * video-js-hls | ||
3 | * | ||
4 | * Copyright (c) 2014 Brightcove | ||
5 | * All rights reserved. | ||
6 | */ | ||
7 | |||
8 | /** | ||
2 | * A stream-based mp2t to mp4 converter. This utility is used to | 9 | * A stream-based mp2t to mp4 converter. This utility is used to |
3 | * deliver mp4s to a SourceBuffer on platforms that support native | 10 | * deliver mp4s to a SourceBuffer on platforms that support native |
4 | * Media Source Extensions. The equivalent process for Flash-based | 11 | * Media Source Extensions. The equivalent process for Flash-based |
... | @@ -7,7 +14,7 @@ | ... | @@ -7,7 +14,7 @@ |
7 | (function(window, videojs, undefined) { | 14 | (function(window, videojs, undefined) { |
8 | 'use strict'; | 15 | 'use strict'; |
9 | 16 | ||
10 | var PacketStream, ParseStream, Transmuxer, MP2T_PACKET_LENGTH; | 17 | var PacketStream, ParseStream, MP2T_PACKET_LENGTH; |
11 | 18 | ||
12 | MP2T_PACKET_LENGTH = 188; // bytes | 19 | MP2T_PACKET_LENGTH = 188; // bytes |
13 | 20 | ||
... | @@ -232,20 +239,11 @@ ParseStream = function() { | ... | @@ -232,20 +239,11 @@ ParseStream = function() { |
232 | }; | 239 | }; |
233 | ParseStream.prototype = new videojs.Hls.Stream(); | 240 | ParseStream.prototype = new videojs.Hls.Stream(); |
234 | 241 | ||
235 | Transmuxer = function() { | ||
236 | Transmuxer.prototype.init.call(this); | ||
237 | this.push = function() { | ||
238 | this.mp4 = new Uint8Array(); | ||
239 | }; | ||
240 | }; | ||
241 | Transmuxer.prototype = new videojs.Hls.Stream(); | ||
242 | |||
243 | window.videojs.mp2t = { | 242 | window.videojs.mp2t = { |
244 | PAT_PID: 0x0000, | 243 | PAT_PID: 0x0000, |
245 | MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH, | 244 | MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH, |
246 | H264_STREAM_TYPE: 0x1b, | 245 | H264_STREAM_TYPE: 0x1b, |
247 | ADTS_STREAM_TYPE: 0x0f, | 246 | ADTS_STREAM_TYPE: 0x0f, |
248 | Transmuxer: Transmuxer, | ||
249 | PacketStream: PacketStream, | 247 | PacketStream: PacketStream, |
250 | ParseStream: ParseStream | 248 | ParseStream: ParseStream |
251 | }; | 249 | }; | ... | ... |
... | @@ -90,12 +90,13 @@ module.exports = function(config) { | ... | @@ -90,12 +90,13 @@ module.exports = function(config) { |
90 | '../src/playlist-loader.js', | 90 | '../src/playlist-loader.js', |
91 | '../src/decrypter.js', | 91 | '../src/decrypter.js', |
92 | '../src/transmuxer.js', | 92 | '../src/transmuxer.js', |
93 | '../src/mp4-generator.js', | ||
93 | '../tmp/manifests.js', | 94 | '../tmp/manifests.js', |
94 | '../tmp/expected.js', | 95 | '../tmp/expected.js', |
95 | 'tsSegment-bc.js', | 96 | 'tsSegment-bc.js', |
96 | '../src/bin-utils.js', | 97 | '../src/bin-utils.js', |
98 | '../test/muxer/js/mp4-inspector.js', | ||
97 | '../test/*.js', | 99 | '../test/*.js', |
98 | '../test/muxer/js/mp4-inspector.js' | ||
99 | ], | 100 | ], |
100 | 101 | ||
101 | plugins: [ | 102 | plugins: [ | ... | ... |
... | @@ -55,12 +55,13 @@ module.exports = function(config) { | ... | @@ -55,12 +55,13 @@ module.exports = function(config) { |
55 | '../src/playlist-loader.js', | 55 | '../src/playlist-loader.js', |
56 | '../src/decrypter.js', | 56 | '../src/decrypter.js', |
57 | '../src/transmuxer.js', | 57 | '../src/transmuxer.js', |
58 | '../src/mp4-generator.js', | ||
58 | '../tmp/manifests.js', | 59 | '../tmp/manifests.js', |
59 | '../tmp/expected.js', | 60 | '../tmp/expected.js', |
60 | 'tsSegment-bc.js', | 61 | 'tsSegment-bc.js', |
61 | '../src/bin-utils.js', | 62 | '../src/bin-utils.js', |
63 | '../test/muxer/js/mp4-inspector.js', | ||
62 | '../test/*.js', | 64 | '../test/*.js', |
63 | '../test/muxer/js/mp4-inspector.js' | ||
64 | ], | 65 | ], |
65 | 66 | ||
66 | plugins: [ | 67 | plugins: [ | ... | ... |
test/mp4-generator_test.js
0 → 100644
1 | (function(window, videojs) { | ||
2 | 'use strict'; | ||
3 | /* | ||
4 | ======== A Handy Little QUnit Reference ======== | ||
5 | http://api.qunitjs.com/ | ||
6 | |||
7 | Test methods: | ||
8 | module(name, {[setup][ ,teardown]}) | ||
9 | test(name, callback) | ||
10 | expect(numberOfAssertions) | ||
11 | stop(increment) | ||
12 | start(decrement) | ||
13 | Test assertions: | ||
14 | ok(value, [message]) | ||
15 | equal(actual, expected, [message]) | ||
16 | notEqual(actual, expected, [message]) | ||
17 | deepEqual(actual, expected, [message]) | ||
18 | notDeepEqual(actual, expected, [message]) | ||
19 | strictEqual(actual, expected, [message]) | ||
20 | notStrictEqual(actual, expected, [message]) | ||
21 | throws(block, [expected], [message]) | ||
22 | */ | ||
23 | var | ||
24 | mp4 = videojs.mp4, | ||
25 | inspectMp4 = videojs.inspectMp4; | ||
26 | |||
27 | module('MP4 Generator'); | ||
28 | |||
29 | test('generates a BSMFF ftyp', function() { | ||
30 | var data = mp4.ftyp(), boxes; | ||
31 | |||
32 | ok(data, 'box is not null'); | ||
33 | |||
34 | boxes = inspectMp4(data); | ||
35 | equal(1, boxes.length, 'generated a single box'); | ||
36 | equal(boxes[0].type, 'ftyp', 'generated ftyp type'); | ||
37 | equal(boxes[0].size, data.byteLength, 'generated size'); | ||
38 | equal(boxes[0].majorBrand, 'isom', 'major version is "isom"'); | ||
39 | equal(boxes[0].minorVersion, 1, 'minor version is one'); | ||
40 | }); | ||
41 | |||
42 | test('generates a moov', function() { | ||
43 | var boxes, mvhd, tkhd, mdhd, hdlr, minf, mvex, | ||
44 | data = mp4.moov(100, 600, 300); | ||
45 | |||
46 | ok(data, 'box is not null'); | ||
47 | |||
48 | boxes = inspectMp4(data); | ||
49 | equal(boxes.length, 1, 'generated a single box'); | ||
50 | equal(boxes[0].type, 'moov', 'generated a moov type'); | ||
51 | equal(boxes[0].size, data.byteLength, 'generated size'); | ||
52 | equal(boxes[0].boxes.length, 3, 'generated three sub boxes'); | ||
53 | |||
54 | mvhd = boxes[0].boxes[0]; | ||
55 | equal(mvhd.type, 'mvhd', 'generated a mvhd'); | ||
56 | equal(mvhd.duration, 100, 'wrote the movie header duration'); | ||
57 | |||
58 | equal(boxes[0].boxes[1].type, 'trak', 'generated a trak'); | ||
59 | equal(boxes[0].boxes[1].boxes.length, 2, 'generated two track sub boxes'); | ||
60 | tkhd = boxes[0].boxes[1].boxes[0]; | ||
61 | equal(tkhd.type, 'tkhd', 'generated a tkhd'); | ||
62 | equal(tkhd.duration, 100, 'wrote duration into the track header'); | ||
63 | equal(tkhd.width, 600, 'wrote width into the track header'); | ||
64 | equal(tkhd.height, 300, 'wrote height into the track header'); | ||
65 | |||
66 | equal(boxes[0].boxes[1].boxes[1].type, 'mdia', 'generated an mdia type'); | ||
67 | equal(boxes[0].boxes[1].boxes[1].boxes.length, 3, 'generated three track media sub boxes'); | ||
68 | |||
69 | mdhd = boxes[0].boxes[1].boxes[1].boxes[0]; | ||
70 | equal(mdhd.type, 'mdhd', 'generate an mdhd type'); | ||
71 | equal(mdhd.language, 'und', 'wrote undetermined language'); | ||
72 | equal(mdhd.duration, 100, 'wrote duraiton into the media header'); | ||
73 | |||
74 | hdlr = boxes[0].boxes[1].boxes[1].boxes[1]; | ||
75 | equal(hdlr.type, 'hdlr', 'generate an hdlr type'); | ||
76 | equal(hdlr.handlerType, 'vide', 'wrote a video handler'); | ||
77 | equal(hdlr.name, 'VideoHandler', 'wrote the handler name'); | ||
78 | |||
79 | minf = boxes[0].boxes[1].boxes[1].boxes[2]; | ||
80 | equal(minf.type, 'minf', 'generate an minf type'); | ||
81 | equal(minf.boxes.length, 2, 'generates two minf sub boxes'); | ||
82 | deepEqual({ | ||
83 | type: 'dinf', | ||
84 | size: 24, | ||
85 | boxes: [{ | ||
86 | type: 'dref', | ||
87 | size: 16, | ||
88 | version: 0, | ||
89 | flags: new Uint8Array([0, 0, 0]), | ||
90 | dataReferences: [] | ||
91 | }] | ||
92 | }, minf.boxes[0], 'generates a dinf'); | ||
93 | |||
94 | equal(minf.boxes[1].type, 'stbl', 'generates an stbl type'); | ||
95 | deepEqual({ | ||
96 | type: 'stbl', | ||
97 | size: 134, | ||
98 | boxes: [{ | ||
99 | type: 'stsd', | ||
100 | size: 102, | ||
101 | version: 0, | ||
102 | flags: new Uint8Array([0, 0, 0]), | ||
103 | sampleDescriptions: [{ | ||
104 | dataReferenceIndex: 1, | ||
105 | width: 600, | ||
106 | height: 300, | ||
107 | horizresolution: 72, | ||
108 | vertresolution: 72, | ||
109 | frameCount: 1, | ||
110 | depth: 24, | ||
111 | size: 86, | ||
112 | type: 'avc1' | ||
113 | }] | ||
114 | }, { | ||
115 | type: 'stts', | ||
116 | size: 8, | ||
117 | timeToSamples: [] | ||
118 | }, { | ||
119 | type: 'stsc', | ||
120 | size: 8, | ||
121 | sampleToChunks: [] | ||
122 | }, { | ||
123 | type: 'stco', | ||
124 | size: 8, | ||
125 | chunkOffsets: [] | ||
126 | }] | ||
127 | }, minf.boxes[1], 'generates a stbl'); | ||
128 | |||
129 | |||
130 | mvex = boxes[0].boxes[2]; | ||
131 | equal(mvex.type, 'mvex', 'generates an mvex type'); | ||
132 | deepEqual({ | ||
133 | type: 'mvex', | ||
134 | size: 40, | ||
135 | boxes: [{ | ||
136 | type: 'trex', | ||
137 | size: 32, | ||
138 | version: 0, | ||
139 | flags: new Uint8Array([0, 0, 0]), | ||
140 | trackId: 1, | ||
141 | defaultSampleDescriptionIndex: 1, | ||
142 | defaultSampleDuration: 0, | ||
143 | defaultSampleSize: 0, | ||
144 | sampleDependsOn: 0, | ||
145 | sampleIsDependedOn: 0, | ||
146 | sampleHasRedundancy: 0, | ||
147 | samplePaddingValue: 0, | ||
148 | sampleIsDifferenceSample: true, | ||
149 | sampleDegradationPriority: 1 | ||
150 | }] | ||
151 | }, mvex, 'writes a movie extends box'); | ||
152 | }); | ||
153 | |||
154 | test('generates an initialization segment', function() { | ||
155 | var | ||
156 | data = mp4.initSegment(), | ||
157 | init; | ||
158 | |||
159 | init = videojs.inspectMp4(data); | ||
160 | equal(init.length, 2, 'generated two boxes'); | ||
161 | equal(init[0].type, 'ftyp', 'generated a ftyp box'); | ||
162 | equal(init[1].type, 'moov', 'generated a moov box'); | ||
163 | }); | ||
164 | |||
165 | |||
166 | })(window, window.videojs); |
... | @@ -108,7 +108,7 @@ var | ... | @@ -108,7 +108,7 @@ var |
108 | 0x00, 0x00, 0x01, 0x2c, // 300 = 0x12c width | 108 | 0x00, 0x00, 0x01, 0x2c, // 300 = 0x12c width |
109 | 0x00, 0x00, 0x00, 0x96), // 150 = 0x96 height | 109 | 0x00, 0x00, 0x00, 0x96), // 150 = 0x96 height |
110 | mdhd0 = box('mdhd', | 110 | mdhd0 = box('mdhd', |
111 | 0x00, // version 1 | 111 | 0x00, // version 0 |
112 | 0x00, 0x00, 0x00, // flags | 112 | 0x00, 0x00, 0x00, // flags |
113 | 0x00, 0x00, 0x00, 0x02, // creation_time | 113 | 0x00, 0x00, 0x00, 0x02, // creation_time |
114 | 0x00, 0x00, 0x00, 0x03, // modification_time | 114 | 0x00, 0x00, 0x00, 0x03, // modification_time |
... | @@ -137,14 +137,14 @@ test('can parse a Box', function() { | ... | @@ -137,14 +137,14 @@ test('can parse a Box', function() { |
137 | 137 | ||
138 | test('can parse an ftyp', function() { | 138 | test('can parse an ftyp', function() { |
139 | deepEqual(videojs.inspectMp4(new Uint8Array(box('ftyp', | 139 | deepEqual(videojs.inspectMp4(new Uint8Array(box('ftyp', |
140 | 0x00, 0x00, 0x00, 0x01, // major brand | 140 | 0x61, 0x76, 0x63, 0x31, // major brand |
141 | 0x00, 0x00, 0x00, 0x02, // minor version | 141 | 0x00, 0x00, 0x00, 0x02, // minor version |
142 | 0x00, 0x00, 0x00, 0x03, // compatible brands | 142 | 0x00, 0x00, 0x00, 0x03, // compatible brands |
143 | 0x00, 0x00, 0x00, 0x04 // compatible brands | 143 | 0x00, 0x00, 0x00, 0x04 // compatible brands |
144 | ))), [{ | 144 | ))), [{ |
145 | type: 'ftyp', | 145 | type: 'ftyp', |
146 | size: 4 * 6, | 146 | size: 4 * 6, |
147 | majorBrand: 1, | 147 | majorBrand: 'avc1', |
148 | minorVersion: 2, | 148 | minorVersion: 2, |
149 | compatibleBrands: [3, 4] | 149 | compatibleBrands: [3, 4] |
150 | }], 'parsed an ftyp'); | 150 | }], 'parsed an ftyp'); |
... | @@ -401,15 +401,20 @@ test('can parse a moov', function() { | ... | @@ -401,15 +401,20 @@ test('can parse a moov', function() { |
401 | size: 24, | 401 | size: 24, |
402 | boxes: [{ | 402 | boxes: [{ |
403 | type: 'dref', | 403 | type: 'dref', |
404 | version: 1, | ||
405 | flags: new Uint8Array([0, 0, 0]), | ||
404 | dataReferences: [], | 406 | dataReferences: [], |
405 | size: 16 | 407 | size: 16 |
406 | }]}, { | 408 | }] |
409 | }, { | ||
407 | type: 'stbl', | 410 | type: 'stbl', |
408 | size: 72, | 411 | size: 72, |
409 | boxes: [{ | 412 | boxes: [{ |
410 | type: 'stsd', | 413 | type: 'stsd', |
414 | size: 16, | ||
415 | version: 1, | ||
416 | flags: new Uint8Array([0, 0, 0]), | ||
411 | sampleDescriptions: [], | 417 | sampleDescriptions: [], |
412 | size: 16 | ||
413 | }, { | 418 | }, { |
414 | type: 'stts', | 419 | type: 'stts', |
415 | timeToSamples: [], | 420 | timeToSamples: [], |
... | @@ -430,11 +435,94 @@ test('can parse a moov', function() { | ... | @@ -430,11 +435,94 @@ test('can parse a moov', function() { |
430 | }], 'parsed a moov'); | 435 | }], 'parsed a moov'); |
431 | }); | 436 | }); |
432 | 437 | ||
438 | test('can parse an mvex', function() { | ||
439 | var mvex = | ||
440 | box('mvex', | ||
441 | box('trex', | ||
442 | 0x00, // version | ||
443 | 0x00, 0x00, 0x00, // flags | ||
444 | 0x00, 0x00, 0x00, 0x01, // track_ID | ||
445 | 0x00, 0x00, 0x00, 0x01, // default_sample_description_index | ||
446 | 0x00, 0x00, 0x00, 0x02, // default_sample_duration | ||
447 | 0x00, 0x00, 0x00, 0x03, // default_sample_size | ||
448 | 0x00, 0x61, 0x00, 0x01)); // default_sample_flags | ||
449 | deepEqual(videojs.inspectMp4(new Uint8Array(mvex)), [{ | ||
450 | type: 'mvex', | ||
451 | size: 40, | ||
452 | boxes: [{ | ||
453 | type: 'trex', | ||
454 | size: 32, | ||
455 | version: 0, | ||
456 | flags: new Uint8Array([0, 0, 0]), | ||
457 | trackId: 1, | ||
458 | defaultSampleDescriptionIndex: 1, | ||
459 | defaultSampleDuration: 2, | ||
460 | defaultSampleSize: 3, | ||
461 | sampleDependsOn: 0, | ||
462 | sampleIsDependedOn: 1, | ||
463 | sampleHasRedundancy: 2, | ||
464 | samplePaddingValue: 0, | ||
465 | sampleIsDifferenceSample: true, | ||
466 | sampleDegradationPriority: 1 | ||
467 | }] | ||
468 | }], 'parsed an mvex'); | ||
469 | }); | ||
470 | |||
471 | test('can parse a video stsd', function() { | ||
472 | var data = box('stsd', | ||
473 | 0x00, // version 0 | ||
474 | 0x00, 0x00, 0x00, // flags | ||
475 | 0x00, 0x00, 0x00, 0x01, | ||
476 | box('avc1', | ||
477 | 0x00, 0x00, 0x00, | ||
478 | 0x00, 0x00, 0x00, // reserved | ||
479 | 0x00, 0x01, // data_reference_index | ||
480 | 0x00, 0x00, // pre_defined | ||
481 | 0x00, 0x00, // reserved | ||
482 | 0x00, 0x00, 0x00, 0x00, | ||
483 | 0x00, 0x00, 0x00, 0x00, | ||
484 | 0x00, 0x00, 0x00, 0x00, // pre_defined | ||
485 | 0x01, 0x2c, // width = 300 | ||
486 | 0x00, 0x96, // height = 150 | ||
487 | 0x00, 0x48, 0x00, 0x00, // horizresolution | ||
488 | 0x00, 0x48, 0x00, 0x00, // vertresolution | ||
489 | 0x00, 0x00, 0x00, 0x00, // reserved | ||
490 | 0x00, 0x01, // frame_count | ||
491 | 0x04, | ||
492 | typeBytes('avc1'), | ||
493 | 0x00, 0x00, 0x00, 0x00, | ||
494 | 0x00, 0x00, 0x00, 0x00, | ||
495 | 0x00, 0x00, 0x00, 0x00, | ||
496 | 0x00, 0x00, 0x00, 0x00, | ||
497 | 0x00, 0x00, 0x00, 0x00, | ||
498 | 0x00, 0x00, 0x00, 0x00, | ||
499 | 0x00, 0x00, 0x00, // compressorname | ||
500 | 0x00, 0x18, // depth = 24 | ||
501 | 0x11, 0x11)); // pre_defined | ||
502 | deepEqual(videojs.inspectMp4(new Uint8Array(data)), [{ | ||
503 | type: 'stsd', | ||
504 | size: 102, | ||
505 | version: 0, | ||
506 | flags: new Uint8Array([0, 0, 0]), | ||
507 | sampleDescriptions: [{ | ||
508 | type: 'avc1', | ||
509 | size: 86, | ||
510 | dataReferenceIndex: 1, | ||
511 | width: 300, | ||
512 | height: 150, | ||
513 | horizresolution: 72, | ||
514 | vertresolution: 72, | ||
515 | frameCount: 1, | ||
516 | depth: 24 | ||
517 | }] | ||
518 | }]); | ||
519 | }); | ||
520 | |||
433 | test('can parse a series of boxes', function() { | 521 | test('can parse a series of boxes', function() { |
434 | var ftyp = [ | 522 | var ftyp = [ |
435 | 0x00, 0x00, 0x00, 0x18 // size 4 * 6 = 24 | 523 | 0x00, 0x00, 0x00, 0x18 // size 4 * 6 = 24 |
436 | ].concat(typeBytes('ftyp')).concat([ | 524 | ].concat(typeBytes('ftyp')).concat([ |
437 | 0x00, 0x00, 0x00, 0x01, // major brand | 525 | 0x69, 0x73, 0x6f, 0x6d, // major brand |
438 | 0x00, 0x00, 0x00, 0x02, // minor version | 526 | 0x00, 0x00, 0x00, 0x02, // minor version |
439 | 0x00, 0x00, 0x00, 0x03, // compatible brands | 527 | 0x00, 0x00, 0x00, 0x03, // compatible brands |
440 | 0x00, 0x00, 0x00, 0x04, // compatible brands | 528 | 0x00, 0x00, 0x00, 0x04, // compatible brands |
... | @@ -444,13 +532,13 @@ test('can parse a series of boxes', function() { | ... | @@ -444,13 +532,13 @@ test('can parse a series of boxes', function() { |
444 | [{ | 532 | [{ |
445 | type: 'ftyp', | 533 | type: 'ftyp', |
446 | size: 4 * 6, | 534 | size: 4 * 6, |
447 | majorBrand: 1, | 535 | majorBrand: 'isom', |
448 | minorVersion: 2, | 536 | minorVersion: 2, |
449 | compatibleBrands: [3, 4] | 537 | compatibleBrands: [3, 4] |
450 | },{ | 538 | },{ |
451 | type: 'ftyp', | 539 | type: 'ftyp', |
452 | size: 4 * 6, | 540 | size: 4 * 6, |
453 | majorBrand: 1, | 541 | majorBrand: 'isom', |
454 | minorVersion: 2, | 542 | minorVersion: 2, |
455 | compatibleBrands: [3, 4] | 543 | compatibleBrands: [3, 4] |
456 | }], | 544 | }], | ... | ... |
... | @@ -19,11 +19,26 @@ var | ... | @@ -19,11 +19,26 @@ var |
19 | 19 | ||
20 | // registry of handlers for individual mp4 box types | 20 | // registry of handlers for individual mp4 box types |
21 | parse = { | 21 | parse = { |
22 | // codingname, not a first-class box type. stsd entries share the | ||
23 | // same format as real boxes so the parsing infrastructure can be | ||
24 | // shared | ||
25 | avc1: function(data) { | ||
26 | var view = new DataView(data.buffer, data.byteOffset, data.byteLength); | ||
27 | return { | ||
28 | dataReferenceIndex: view.getUint16(6), | ||
29 | width: view.getUint16(24), | ||
30 | height: view.getUint16(26), | ||
31 | horizresolution: view.getUint16(28) + (view.getUint16(30) / 16), | ||
32 | vertresolution: view.getUint16(32) + (view.getUint16(34) / 16), | ||
33 | frameCount: view.getUint16(40), | ||
34 | depth: view.getUint16(74) | ||
35 | }; | ||
36 | }, | ||
22 | ftyp: function(data) { | 37 | ftyp: function(data) { |
23 | var | 38 | var |
24 | view = new DataView(data.buffer, data.byteOffset, data.byteLength), | 39 | view = new DataView(data.buffer, data.byteOffset, data.byteLength), |
25 | result = { | 40 | result = { |
26 | majorBrand: view.getUint32(0), | 41 | majorBrand: parseType(data.subarray(0, 4)), |
27 | minorVersion: view.getUint32(4), | 42 | minorVersion: view.getUint32(4), |
28 | compatibleBrands: [] | 43 | compatibleBrands: [] |
29 | }, | 44 | }, |
... | @@ -41,6 +56,8 @@ var | ... | @@ -41,6 +56,8 @@ var |
41 | }, | 56 | }, |
42 | dref: function(data) { | 57 | dref: function(data) { |
43 | return { | 58 | return { |
59 | version: data[0], | ||
60 | flags: new Uint8Array(data.subarray(1, 4)), | ||
44 | dataReferences: [] | 61 | dataReferences: [] |
45 | }; | 62 | }; |
46 | }, | 63 | }, |
... | @@ -129,6 +146,11 @@ var | ... | @@ -129,6 +146,11 @@ var |
129 | boxes: videojs.inspectMp4(data) | 146 | boxes: videojs.inspectMp4(data) |
130 | }; | 147 | }; |
131 | }, | 148 | }, |
149 | mvex: function(data) { | ||
150 | return { | ||
151 | boxes: videojs.inspectMp4(data) | ||
152 | }; | ||
153 | }, | ||
132 | mvhd: function(data) { | 154 | mvhd: function(data) { |
133 | var | 155 | var |
134 | view = new DataView(data.buffer, data.byteOffset, data.byteLength), | 156 | view = new DataView(data.buffer, data.byteOffset, data.byteLength), |
... | @@ -185,6 +207,23 @@ var | ... | @@ -185,6 +207,23 @@ var |
185 | boxes: videojs.inspectMp4(data) | 207 | boxes: videojs.inspectMp4(data) |
186 | }; | 208 | }; |
187 | }, | 209 | }, |
210 | trex: function(data) { | ||
211 | var view = new DataView(data.buffer, data.byteOffset, data.byteLength); | ||
212 | return { | ||
213 | version: data[0], | ||
214 | flags: new Uint8Array(data.subarray(1, 4)), | ||
215 | trackId: view.getUint32(4), | ||
216 | defaultSampleDescriptionIndex: view.getUint32(8), | ||
217 | defaultSampleDuration: view.getUint32(12), | ||
218 | defaultSampleSize: view.getUint32(16), | ||
219 | sampleDependsOn: data[20] & 0x03, | ||
220 | sampleIsDependedOn: (data[21] & 0xc0) >> 6, | ||
221 | sampleHasRedundancy: (data[21] & 0x30) >> 4, | ||
222 | samplePaddingValue: (data[21] & 0x0e) >> 1, | ||
223 | sampleIsDifferenceSample: !!(data[21] & 0x01), | ||
224 | sampleDegradationPriority: view.getUint16(22) | ||
225 | }; | ||
226 | }, | ||
188 | stbl: function(data) { | 227 | stbl: function(data) { |
189 | return { | 228 | return { |
190 | boxes: videojs.inspectMp4(data) | 229 | boxes: videojs.inspectMp4(data) |
... | @@ -202,7 +241,9 @@ var | ... | @@ -202,7 +241,9 @@ var |
202 | }, | 241 | }, |
203 | stsd: function(data) { | 242 | stsd: function(data) { |
204 | return { | 243 | return { |
205 | sampleDescriptions: [] | 244 | version: data[0], |
245 | flags: new Uint8Array(data.subarray(1, 4)), | ||
246 | sampleDescriptions: videojs.inspectMp4(data.subarray(8)) | ||
206 | }; | 247 | }; |
207 | }, | 248 | }, |
208 | stts: function(data) { | 249 | stts: function(data) { | ... | ... |
... | @@ -106,6 +106,7 @@ | ... | @@ -106,6 +106,7 @@ |
106 | </script> | 106 | </script> |
107 | <script src="../../src/stream.js"></script> | 107 | <script src="../../src/stream.js"></script> |
108 | <script src="../../src/transmuxer.js"></script> | 108 | <script src="../../src/transmuxer.js"></script> |
109 | <script src="../../src/mp4-generator.js"></script> | ||
109 | <script src="js/mp4-inspector.js"></script> | 110 | <script src="js/mp4-inspector.js"></script> |
110 | 111 | ||
111 | <script src="../../src/bin-utils.js"></script> | 112 | <script src="../../src/bin-utils.js"></script> |
... | @@ -118,13 +119,9 @@ | ... | @@ -118,13 +119,9 @@ |
118 | workingBoxes = document.querySelector('.working-boxes'), | 119 | workingBoxes = document.querySelector('.working-boxes'), |
119 | 120 | ||
120 | vjsOutput = document.querySelector('.vjs-hls-output'), | 121 | vjsOutput = document.querySelector('.vjs-hls-output'), |
121 | workingOutput = document.querySelector('.working-output'), | 122 | workingOutput = document.querySelector('.working-output'); |
122 | 123 | ||
123 | tagTypes = { | 124 | // webkit dash-mp4 mime: 'video/mp4;codecs=avc1.4d0020,mp4a.40.2' |
124 | 0x08: 'audio', | ||
125 | 0x09: 'video', | ||
126 | 0x12: 'metadata' | ||
127 | }; | ||
128 | 125 | ||
129 | videojs.log = console.log.bind(console); | 126 | videojs.log = console.log.bind(console); |
130 | 127 | ||
... | @@ -132,19 +129,43 @@ | ... | @@ -132,19 +129,43 @@ |
132 | var reader = new FileReader(); | 129 | var reader = new FileReader(); |
133 | reader.addEventListener('loadend', function() { | 130 | reader.addEventListener('loadend', function() { |
134 | var segment = new Uint8Array(reader.result), | 131 | var segment = new Uint8Array(reader.result), |
135 | transmuxer = new videojs.Hls.Transmuxer(), | 132 | packetizer = new videojs.mp2t.PacketStream(), |
133 | parser = new videojs.mp2t.ParseStream(), | ||
136 | hex = ''; | 134 | hex = ''; |
137 | 135 | ||
138 | transmuxer.push(segment); | 136 | packetizer.pipe(parser); |
137 | packetizer.push(segment); | ||
139 | 138 | ||
140 | // clear old boxes info | 139 | // clear old boxes info |
141 | vjsBoxes.innerHTML = JSON.stringify(videojs.inspectMp4(transmuxer.mp4), null, ' '); | ||
142 | 140 | ||
143 | // write out the result | 141 | // write out the result |
144 | hex += '<pre>'; | 142 | hex += '<pre>'; |
145 | hex += videojs.Hls.utils.hexDump(transmuxer.mp4); | 143 | // hex += videojs.Hls.utils.hexDump(transmuxer.mp4); |
146 | hex += '</pre>'; | 144 | hex += '</pre>'; |
147 | vjsOutput.innerHTML = hex; | 145 | vjsOutput.innerHTML = hex; |
146 | |||
147 | // XXX media source testing | ||
148 | |||
149 | vjsBoxes.innerHTML = JSON.stringify(videojs.inspectMp4(videojs.mp4.initSegment()), null, ' '); | ||
150 | |||
151 | var video = document.createElement('video'); | ||
152 | var mediaSource = new MediaSource(); | ||
153 | mediaSource.addEventListener('sourceopen', function() { | ||
154 | var buffer = mediaSource.addSourceBuffer('video/mp4;codecs=avc1.4d0020,mp4a.40.2'); | ||
155 | buffer.addEventListener('updatestart', console.log.bind(console)); | ||
156 | buffer.addEventListener('updateend', console.log.bind(console)); | ||
157 | buffer.addEventListener('error', console.log.bind(console)); | ||
158 | buffer.appendBuffer(videojs.mp4.initSegment()); | ||
159 | console.log('done', mediaSource, buffer, video.error); | ||
160 | window.vjsMediaSource = mediaSource; | ||
161 | window.vjsSourceBuffer = buffer; | ||
162 | window.vjsVideo = video; | ||
163 | }); | ||
164 | mediaSource.addEventListener('error', console.log.bind(console)); | ||
165 | mediaSource.addEventListener('opened', console.log.bind(console)); | ||
166 | mediaSource.addEventListener('closed', console.log.bind(console)); | ||
167 | mediaSource.addEventListener('sourceended', console.log.bind(console)); | ||
168 | video.src = URL.createObjectURL(mediaSource); | ||
148 | }); | 169 | }); |
149 | reader.readAsArrayBuffer(this.files[0]); | 170 | reader.readAsArrayBuffer(this.files[0]); |
150 | }, false); | 171 | }, false); | ... | ... |
... | @@ -24,9 +24,7 @@ var | ... | @@ -24,9 +24,7 @@ var |
24 | PacketStream = videojs.mp2t.PacketStream, | 24 | PacketStream = videojs.mp2t.PacketStream, |
25 | packetStream, | 25 | packetStream, |
26 | ParseStream = videojs.mp2t.ParseStream, | 26 | ParseStream = videojs.mp2t.ParseStream, |
27 | parseStream, | 27 | parseStream; |
28 | Transmuxer = videojs.mp2t.Transmuxer, | ||
29 | transmuxer; | ||
30 | 28 | ||
31 | module('MP2T Packet Stream', { | 29 | module('MP2T Packet Stream', { |
32 | setup: function() { | 30 | setup: function() { |
... | @@ -394,17 +392,4 @@ test('parses an elementary stream packet without a pts or dts', function() { | ... | @@ -394,17 +392,4 @@ test('parses an elementary stream packet without a pts or dts', function() { |
394 | ok(!packet.dts, 'did not parse a dts'); | 392 | ok(!packet.dts, 'did not parse a dts'); |
395 | }); | 393 | }); |
396 | 394 | ||
397 | module('MP4 Transmuxer', { | ||
398 | setup: function() { | ||
399 | transmuxer = new Transmuxer(); | ||
400 | } | ||
401 | }); | ||
402 | |||
403 | test('can mux an empty mp2t', function() { | ||
404 | transmuxer.push(new Uint8Array()); | ||
405 | |||
406 | ok(transmuxer.mp4, 'produced a non-null result'); | ||
407 | strictEqual(transmuxer.mp4.byteLength, 0, 'produced an empty mp4'); | ||
408 | }); | ||
409 | |||
410 | })(window, window.videojs); | 395 | })(window, window.videojs); | ... | ... |
... | @@ -44,6 +44,7 @@ | ... | @@ -44,6 +44,7 @@ |
44 | 44 | ||
45 | <!-- mp4 utilities --> | 45 | <!-- mp4 utilities --> |
46 | <script src="../src/transmuxer.js"></script> | 46 | <script src="../src/transmuxer.js"></script> |
47 | <script src="../src/mp4-generator.js"></script> | ||
47 | <script src="muxer/js/mp4-inspector.js"></script> | 48 | <script src="muxer/js/mp4-inspector.js"></script> |
48 | 49 | ||
49 | <!-- Test cases --> | 50 | <!-- Test cases --> |
... | @@ -66,6 +67,7 @@ | ... | @@ -66,6 +67,7 @@ |
66 | <script src="decrypter_test.js"></script> | 67 | <script src="decrypter_test.js"></script> |
67 | <script src="transmuxer_test.js"></script> | 68 | <script src="transmuxer_test.js"></script> |
68 | <script src="mp4-inspector_test.js"></script> | 69 | <script src="mp4-inspector_test.js"></script> |
70 | <script src="mp4-generator_test.js"></script> | ||
69 | </head> | 71 | </head> |
70 | <body> | 72 | <body> |
71 | <div id="qunit"></div> | 73 | <div id="qunit"></div> | ... | ... |
-
Please register or sign in to post a comment