Merge pull request #8 from dlapalomento/feature/perf-fixes
Grow FLV tags as necessary and make some performance tweaks
Showing
7 changed files
with
293 additions
and
113 deletions
... | @@ -46,7 +46,7 @@ module.exports = function(grunt) { | ... | @@ -46,7 +46,7 @@ module.exports = function(grunt) { |
46 | }, | 46 | }, |
47 | }, | 47 | }, |
48 | qunit: { | 48 | qunit: { |
49 | files: ['test/**/*.html'] | 49 | files: ['test/**/*.html', '!test/perf.html'] |
50 | }, | 50 | }, |
51 | jshint: { | 51 | jshint: { |
52 | gruntfile: { | 52 | gruntfile: { | ... | ... |
... | @@ -52,7 +52,7 @@ | ... | @@ -52,7 +52,7 @@ |
52 | videojs.options.flash.swf = 'node_modules/videojs-media-sources/video-js-with-mse.swf'; | 52 | videojs.options.flash.swf = 'node_modules/videojs-media-sources/video-js-with-mse.swf'; |
53 | video = videojs('video',{},function(){ | 53 | video = videojs('video',{},function(){ |
54 | this.playbackController = new window.videojs.hls.HLSPlaybackController(this); | 54 | this.playbackController = new window.videojs.hls.HLSPlaybackController(this); |
55 | this.playbackController.loadManifest('http://localhost:7070/test/basic-playback/zencoder/gogo/manifest.m3u8', function(data) { | 55 | this.playbackController.loadManifest('test/fixtures/prog_index.m3u8', function(data) { |
56 | console.log(data); | 56 | console.log(data); |
57 | }); | 57 | }); |
58 | }); | 58 | }); | ... | ... |
1 | (function(window) { | 1 | (function(window) { |
2 | 2 | ||
3 | var hls = window.videojs.hls; | 3 | var |
4 | hls = window.videojs.hls, | ||
5 | |||
6 | // commonly used metadata properties | ||
7 | widthBytes = new Uint8Array('width'.length), | ||
8 | heightBytes = new Uint8Array('height'.length), | ||
9 | videocodecidBytes = new Uint8Array('videocodecid'.length), | ||
10 | i; | ||
11 | |||
12 | // calculating the bytes of common metadata names ahead of time makes the | ||
13 | // corresponding writes faster because we don't have to loop over the | ||
14 | // characters | ||
15 | // re-test with test/perf.html if you're planning on changing this | ||
16 | for (i in 'width') { | ||
17 | widthBytes[i] = 'width'.charCodeAt(i); | ||
18 | } | ||
19 | for (i in 'height') { | ||
20 | heightBytes[i] = 'height'.charCodeAt(i); | ||
21 | } | ||
22 | for (i in 'videocodecid') { | ||
23 | videocodecidBytes[i] = 'videocodecid'.charCodeAt(i); | ||
24 | } | ||
4 | 25 | ||
5 | // (type:uint, extraData:Boolean = false) extends ByteArray | 26 | // (type:uint, extraData:Boolean = false) extends ByteArray |
6 | hls.FlvTag = function(type, extraData) { | 27 | hls.FlvTag = function(type, extraData) { |
7 | var | 28 | var |
8 | // Counter if this is a metadata tag, nal start marker if this is a video | 29 | // Counter if this is a metadata tag, nal start marker if this is a video |
9 | // tag. unused if this is an audio tag | 30 | // tag. unused if this is an audio tag |
10 | adHoc = 0; // :uint | 31 | adHoc = 0, // :uint |
32 | |||
33 | // checks whether the FLV tag has enough capacity to accept the proposed | ||
34 | // write and re-allocates the internal buffers if necessary | ||
35 | prepareWrite = function(flv, count) { | ||
36 | var | ||
37 | bytes, | ||
38 | minLength = flv.position + count; | ||
39 | if (minLength < flv.bytes.byteLength) { | ||
40 | // there's enough capacity so do nothing | ||
41 | return; | ||
42 | } | ||
43 | |||
44 | // allocate a new buffer and copy over the data that will not be modified | ||
45 | bytes = new Uint8Array(minLength * 2); | ||
46 | bytes.set(flv.bytes.subarray(0, flv.position), 0); | ||
47 | flv.bytes = bytes; | ||
48 | flv.view = new DataView(flv.bytes.buffer); | ||
49 | }; | ||
11 | 50 | ||
12 | this.keyFrame = false; // :Boolean | 51 | this.keyFrame = false; // :Boolean |
13 | 52 | ||
... | @@ -27,7 +66,6 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -27,7 +66,6 @@ hls.FlvTag = function(type, extraData) { |
27 | throw("Error Unknown TagType"); | 66 | throw("Error Unknown TagType"); |
28 | } | 67 | } |
29 | 68 | ||
30 | // XXX: I have no idea if 16k is enough to buffer arbitrary FLV tags | ||
31 | this.bytes = new Uint8Array(16384); | 69 | this.bytes = new Uint8Array(16384); |
32 | this.view = new DataView(this.bytes.buffer); | 70 | this.view = new DataView(this.bytes.buffer); |
33 | this.bytes[0] = type; | 71 | this.bytes[0] = type; |
... | @@ -37,24 +75,26 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -37,24 +75,26 @@ hls.FlvTag = function(type, extraData) { |
37 | // presentation timestamp | 75 | // presentation timestamp |
38 | this.pts = 0; | 76 | this.pts = 0; |
39 | // decoder timestamp | 77 | // decoder timestamp |
40 | this.dts = 0; | 78 | this.dts = 0; |
41 | 79 | ||
42 | // ByteArray#writeBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0) | 80 | // ByteArray#writeBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0) |
43 | this.writeBytes = function(bytes, offset, length) { | 81 | this.writeBytes = function(bytes, offset, length) { |
44 | offset = offset || 0; | 82 | var |
83 | start = offset || 0, | ||
84 | end; | ||
45 | length = length || bytes.byteLength; | 85 | length = length || bytes.byteLength; |
86 | end = start + length; | ||
87 | |||
88 | prepareWrite(this, length); | ||
89 | this.bytes.set(bytes.subarray(start, end), this.position); | ||
46 | 90 | ||
47 | try { | ||
48 | this.bytes.set(bytes.subarray(offset, offset + length), this.position); | ||
49 | } catch(e) { | ||
50 | throw e; | ||
51 | } | ||
52 | this.position += length; | 91 | this.position += length; |
53 | this.length = Math.max(this.length, this.position); | 92 | this.length = Math.max(this.length, this.position); |
54 | }; | 93 | }; |
55 | 94 | ||
56 | // ByteArray#writeByte(value:int):void | 95 | // ByteArray#writeByte(value:int):void |
57 | this.writeByte = function(byte) { | 96 | this.writeByte = function(byte) { |
97 | prepareWrite(this, 1); | ||
58 | this.bytes[this.position] = byte; | 98 | this.bytes[this.position] = byte; |
59 | this.position++; | 99 | this.position++; |
60 | this.length = Math.max(this.length, this.position); | 100 | this.length = Math.max(this.length, this.position); |
... | @@ -62,6 +102,7 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -62,6 +102,7 @@ hls.FlvTag = function(type, extraData) { |
62 | 102 | ||
63 | // ByteArray#writeShort(value:int):void | 103 | // ByteArray#writeShort(value:int):void |
64 | this.writeShort = function(short) { | 104 | this.writeShort = function(short) { |
105 | prepareWrite(this, 2); | ||
65 | this.view.setUint16(this.position, short); | 106 | this.view.setUint16(this.position, short); |
66 | this.position += 2; | 107 | this.position += 2; |
67 | this.length = Math.max(this.length, this.position); | 108 | this.length = Math.max(this.length, this.position); |
... | @@ -80,7 +121,7 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -80,7 +121,7 @@ hls.FlvTag = function(type, extraData) { |
80 | if (adHoc === 0) { | 121 | if (adHoc === 0) { |
81 | return 0; | 122 | return 0; |
82 | } | 123 | } |
83 | 124 | ||
84 | return this.length - (adHoc + 4); | 125 | return this.length - (adHoc + 4); |
85 | }; | 126 | }; |
86 | 127 | ||
... | @@ -115,7 +156,7 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -115,7 +156,7 @@ hls.FlvTag = function(type, extraData) { |
115 | this.position = this.length; | 156 | this.position = this.length; |
116 | 157 | ||
117 | if (nalContainer) { | 158 | if (nalContainer) { |
118 | // Add the tag to the NAL unit | 159 | // Add the tag to the NAL unit |
119 | nalContainer.push(this.bytes.subarray(nalStart, nalStart + nalLength)); | 160 | nalContainer.push(this.bytes.subarray(nalStart, nalStart + nalLength)); |
120 | } | 161 | } |
121 | } | 162 | } |
... | @@ -123,20 +164,47 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -123,20 +164,47 @@ hls.FlvTag = function(type, extraData) { |
123 | adHoc = 0; | 164 | adHoc = 0; |
124 | }; | 165 | }; |
125 | 166 | ||
167 | /** | ||
168 | * Write out a 64-bit floating point valued metadata property. This method is | ||
169 | * called frequently during a typical parse and needs to be fast. | ||
170 | */ | ||
126 | // (key:String, val:Number):void | 171 | // (key:String, val:Number):void |
127 | this.writeMetaDataDouble = function(key, val) { | 172 | this.writeMetaDataDouble = function(key, val) { |
128 | var i; | 173 | var i; |
174 | prepareWrite(this, 2 + key.length + 9); | ||
175 | |||
176 | // write size of property name | ||
129 | this.view.setUint16(this.position, key.length); | 177 | this.view.setUint16(this.position, key.length); |
130 | this.position += 2; | 178 | this.position += 2; |
131 | for (i in key) { | 179 | |
132 | console.assert(key.charCodeAt(i) < 255); | 180 | // this next part looks terrible but it improves parser throughput by |
133 | this.bytes[this.position] = key.charCodeAt(i); | 181 | // 10kB/s in my testing |
134 | this.position++; | 182 | |
183 | // write property name | ||
184 | if (key === 'width') { | ||
185 | this.bytes.set(widthBytes, this.position); | ||
186 | this.position += 5; | ||
187 | } else if (key === 'height') { | ||
188 | this.bytes.set(heightBytes, this.position); | ||
189 | this.position += 6; | ||
190 | } else if (key === 'videocodecid') { | ||
191 | this.bytes.set(videocodecidBytes, this.position); | ||
192 | this.position += 12; | ||
193 | } else { | ||
194 | for (i in key) { | ||
195 | this.bytes[this.position] = key.charCodeAt(i); | ||
196 | this.position++; | ||
197 | } | ||
135 | } | 198 | } |
136 | this.view.setUint8(this.position, 0x00); | 199 | |
200 | // skip null byte | ||
137 | this.position++; | 201 | this.position++; |
202 | |||
203 | // write property value | ||
138 | this.view.setFloat64(this.position, val); | 204 | this.view.setFloat64(this.position, val); |
139 | this.position += 8; | 205 | this.position += 8; |
206 | |||
207 | // update flv tag length | ||
140 | this.length = Math.max(this.length, this.position); | 208 | this.length = Math.max(this.length, this.position); |
141 | ++adHoc; | 209 | ++adHoc; |
142 | }; | 210 | }; |
... | @@ -144,13 +212,16 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -144,13 +212,16 @@ hls.FlvTag = function(type, extraData) { |
144 | // (key:String, val:Boolean):void | 212 | // (key:String, val:Boolean):void |
145 | this.writeMetaDataBoolean = function(key, val) { | 213 | this.writeMetaDataBoolean = function(key, val) { |
146 | var i; | 214 | var i; |
215 | prepareWrite(this, 2); | ||
147 | this.view.setUint16(this.position, key.length); | 216 | this.view.setUint16(this.position, key.length); |
148 | this.position += 2; | 217 | this.position += 2; |
149 | for (i in key) { | 218 | for (i in key) { |
150 | console.assert(key.charCodeAt(i) < 255); | 219 | console.assert(key.charCodeAt(i) < 255); |
220 | prepareWrite(this, 1); | ||
151 | this.bytes[this.position] = key.charCodeAt(i); | 221 | this.bytes[this.position] = key.charCodeAt(i); |
152 | this.position++; | 222 | this.position++; |
153 | } | 223 | } |
224 | prepareWrite(this, 2); | ||
154 | this.view.setUint8(this.position, 0x01); | 225 | this.view.setUint8(this.position, 0x01); |
155 | this.position++; | 226 | this.position++; |
156 | this.view.setUint8(this.position, val ? 0x01 : 0x00); | 227 | this.view.setUint8(this.position, val ? 0x01 : 0x00); |
... | @@ -197,10 +268,8 @@ hls.FlvTag = function(type, extraData) { | ... | @@ -197,10 +268,8 @@ hls.FlvTag = function(type, extraData) { |
197 | this.position++; | 268 | this.position++; |
198 | this.view.setUint32(this.position, adHoc); | 269 | this.view.setUint32(this.position, adHoc); |
199 | this.position = this.length; | 270 | this.position = this.length; |
200 | // this.bytes.set([0, 0, 9], this.position); | 271 | this.bytes.set([0, 0, 9], this.position); |
201 | // this.position += 3; | 272 | this.position += 3; // End Data Tag |
202 | this.view.setUint32(this.position, 0x09); // End Data Tag | ||
203 | this.position += 4; | ||
204 | this.length = this.position; | 273 | this.length = this.position; |
205 | break; | 274 | break; |
206 | } | 275 | } |
... | @@ -261,7 +330,7 @@ hls.FlvTag.isKeyFrame = function(tag) { | ... | @@ -261,7 +330,7 @@ hls.FlvTag.isKeyFrame = function(tag) { |
261 | if (hls.FlvTag.isAudioFrame(tag)) { | 330 | if (hls.FlvTag.isAudioFrame(tag)) { |
262 | return true; | 331 | return true; |
263 | } | 332 | } |
264 | 333 | ||
265 | if (hls.FlvTag.isMetaData(tag)) { | 334 | if (hls.FlvTag.isMetaData(tag)) { |
266 | return true; | 335 | return true; |
267 | } | 336 | } | ... | ... |
1 | /* | 1 | /* |
2 | * h264-stream | 2 | * h264-stream |
3 | * | 3 | * |
4 | * | 4 | * |
5 | * Copyright (c) 2013 Brightcove | 5 | * Copyright (c) 2013 Brightcove |
6 | * All rights reserved. | 6 | * All rights reserved. |
... | @@ -62,7 +62,7 @@ | ... | @@ -62,7 +62,7 @@ |
62 | * NAL unit: Network abstraction layer unit. The combination of a NAL | 62 | * NAL unit: Network abstraction layer unit. The combination of a NAL |
63 | * header and an RBSP. | 63 | * header and an RBSP. |
64 | * NAL header: the encapsulation unit for transport-specific metadata in | 64 | * NAL header: the encapsulation unit for transport-specific metadata in |
65 | * an h264 stream. | 65 | * an h264 stream. Exactly one byte. |
66 | * RBSP: raw bit-stream payload. The actual encoded video data. | 66 | * RBSP: raw bit-stream payload. The actual encoded video data. |
67 | * | 67 | * |
68 | * SPS: sequence parameter set. Part of the RBSP. Metadata to be applied | 68 | * SPS: sequence parameter set. Part of the RBSP. Metadata to be applied |
... | @@ -128,7 +128,7 @@ | ... | @@ -128,7 +128,7 @@ |
128 | 128 | ||
129 | pic_width_in_mbs_minus1, // :int | 129 | pic_width_in_mbs_minus1, // :int |
130 | pic_height_in_map_units_minus1, // :int | 130 | pic_height_in_map_units_minus1, // :int |
131 | 131 | ||
132 | frame_mbs_only_flag, // :int | 132 | frame_mbs_only_flag, // :int |
133 | frame_cropping_flag, // :Boolean | 133 | frame_cropping_flag, // :Boolean |
134 | 134 | ||
... | @@ -283,17 +283,17 @@ | ... | @@ -283,17 +283,17 @@ |
283 | 283 | ||
284 | oldExtraData = new H264ExtraData(), // :H264ExtraData | 284 | oldExtraData = new H264ExtraData(), // :H264ExtraData |
285 | newExtraData = new H264ExtraData(), // :H264ExtraData | 285 | newExtraData = new H264ExtraData(), // :H264ExtraData |
286 | 286 | ||
287 | nalUnitType = -1, // :int | 287 | nalUnitType = -1, // :int |
288 | 288 | ||
289 | state; // :uint; | 289 | state; // :uint; |
290 | 290 | ||
291 | this.tags = []; | 291 | this.tags = []; |
292 | 292 | ||
293 | //(pts:uint, dts:uint, dataAligned:Boolean):void | 293 | //(pts:uint, dts:uint, dataAligned:Boolean):void |
294 | this.setNextTimeStamp = function(pts, dts, dataAligned) { | 294 | this.setNextTimeStamp = function(pts, dts, dataAligned) { |
295 | if (0>pts_delta) { | 295 | if (0>pts_delta) { |
296 | // We assume the very first pts is less than 0x8FFFFFFF (max signed | 296 | // We assume the very first pts is less than 0x8FFFFFFF (max signed |
297 | // int32) | 297 | // int32) |
298 | pts_delta = pts; | 298 | pts_delta = pts; |
299 | } | 299 | } |
... | @@ -361,38 +361,40 @@ | ... | @@ -361,38 +361,40 @@ |
361 | // A NAL unit may be split across two TS packets. Look back a bit to | 361 | // A NAL unit may be split across two TS packets. Look back a bit to |
362 | // make sure the prefix of the start code wasn't already written out. | 362 | // make sure the prefix of the start code wasn't already written out. |
363 | if (data[offset] <= 1) { | 363 | if (data[offset] <= 1) { |
364 | nalUnitSize = h264Frame ? h264Frame.nalUnitSize() : 0; | 364 | nalUnitSize = h264Frame ? h264Frame.nalUnitSize() : 0; |
365 | if (nalUnitSize >= 1 && h264Frame.negIndex(1) === 0) { | 365 | if (nalUnitSize >= 1 && h264Frame.negIndex(1) === 0) { |
366 | // ?? ?? 00 | O[01] ?? ?? | 366 | // ?? ?? 00 | O[01] ?? ?? |
367 | if (1 === data[offset] && 2 <= nalUnitSize && 0 === h264Frame.negIndex(2)) { | 367 | if (data[offset] === 1 && |
368 | nalUnitSize >= 2 && | ||
369 | h264Frame.negIndex(2) === 0) { | ||
368 | // ?? 00 00 : 01 | 370 | // ?? 00 00 : 01 |
369 | if (3 <= nalUnitSize && 0 === h264Frame.negIndex(3)) { | 371 | if (3 <= nalUnitSize && 0 === h264Frame.negIndex(3)) { |
370 | h264Frame.length -= 3; // 00 00 00 : 01 | 372 | h264Frame.length -= 3; // 00 00 00 : 01 |
371 | } else { | 373 | } else { |
372 | h264Frame.length -= 2; // 00 00 : 01 | 374 | h264Frame.length -= 2; // 00 00 : 01 |
373 | } | 375 | } |
374 | 376 | ||
375 | state = 3; | 377 | state = 3; |
376 | return this.writeBytes(data, offset + 1, length - 1); | 378 | return this.writeBytes(data, offset + 1, length - 1); |
377 | } | 379 | } |
378 | 380 | ||
379 | if (1 < length && 0 === data[offset] && 1 === data[offset + 1]) { | 381 | if (length > 1 && data[offset] === 0 && data[offset + 1] === 1) { |
380 | // ?? 00 | 00 01 | 382 | // ?? 00 | 00 01 |
381 | if (2 <= nalUnitSize && 0 === h264Frame.negIndex(2)) { | 383 | if (nalUnitSize >= 2 && h264Frame.negIndex(2) === 0) { |
382 | h264Frame.length -= 2; // 00 00 : 00 01 | 384 | h264Frame.length -= 2; // 00 00 : 00 01 |
383 | } else { | 385 | } else { |
384 | h264Frame.length -= 1; // 00 : 00 01 | 386 | h264Frame.length -= 1; // 00 : 00 01 |
385 | } | 387 | } |
386 | 388 | ||
387 | state = 3; | 389 | state = 3; |
388 | return this.writeBytes(data, offset + 2, length - 2); | 390 | return this.writeBytes(data, offset + 2, length - 2); |
389 | } | 391 | } |
390 | 392 | ||
391 | if (2 < length && | 393 | if (length > 2 && |
392 | 0 === data[offset] && | 394 | data[offset] === 0 && |
393 | 0 === data[offset + 1] && | 395 | data[offset + 1] === 0 && |
394 | 1 === data[offset + 2]) { | 396 | data[offset + 2] === 1) { |
395 | // 00 | 00 00 01 | 397 | // 00 : 00 00 01 |
396 | h264Frame.length -= 1; | 398 | h264Frame.length -= 1; |
397 | state = 3; | 399 | state = 3; |
398 | return this.writeBytes(data, offset + 3, length - 3); | 400 | return this.writeBytes(data, offset + 3, length - 3); |
... | @@ -403,19 +405,22 @@ | ... | @@ -403,19 +405,22 @@ |
403 | // bytes a second time. But that case will be VERY rare | 405 | // bytes a second time. But that case will be VERY rare |
404 | state = 2; | 406 | state = 2; |
405 | /* falls through */ | 407 | /* falls through */ |
406 | case 2: // Look for start codes in data | 408 | case 2: |
409 | // Look for start codes in the data from the current offset forward | ||
407 | start = offset; | 410 | start = offset; |
408 | end = start + length; | 411 | end = start + length; |
409 | for (t = end - 3 ; offset < t ;) { | 412 | for (t = end - 3; offset < t;) { |
410 | if (1 < data[offset + 2]) { | 413 | if (data[offset + 2] > 1) { |
411 | offset += 3; // if data[offset + 2] is greater than 1, there is no way a start code can begin before offset+3 | 414 | // if data[offset + 2] is greater than 1, there is no way a start |
412 | } else if (0 !== data[offset + 1]) { | 415 | // code can begin before offset + 3 |
416 | offset += 3; | ||
417 | } else if (data[offset + 1] !== 0) { | ||
413 | offset += 2; | 418 | offset += 2; |
414 | } else if (0 !== data[offset]) { | 419 | } else if (data[offset] !== 0) { |
415 | offset += 1; | 420 | offset += 1; |
416 | } else { | 421 | } else { |
417 | // If we get here we have 00 00 00 or 00 00 01 | 422 | // If we get here we have 00 00 00 or 00 00 01 |
418 | if (1 === data[offset + 2]) { | 423 | if (data[offset + 2] === 1) { |
419 | if (offset > start) { | 424 | if (offset > start) { |
420 | h264Frame.writeBytes(data, start, offset - start); | 425 | h264Frame.writeBytes(data, start, offset - start); |
421 | } | 426 | } |
... | @@ -424,7 +429,9 @@ | ... | @@ -424,7 +429,9 @@ |
424 | return this.writeBytes(data, offset, end - offset); | 429 | return this.writeBytes(data, offset, end - offset); |
425 | } | 430 | } |
426 | 431 | ||
427 | if (end - offset >= 4 && 0 === data[offset + 2] && 1 === data[offset + 3]) { | 432 | if (end - offset >= 4 && |
433 | data[offset + 2] === 0 && | ||
434 | data[offset + 3] === 1) { | ||
428 | if (offset > start) { | 435 | if (offset > start) { |
429 | h264Frame.writeBytes(data, start, offset - start); | 436 | h264Frame.writeBytes(data, start, offset - start); |
430 | } | 437 | } |
... | @@ -482,9 +489,10 @@ | ... | @@ -482,9 +489,10 @@ |
482 | } | 489 | } |
483 | 490 | ||
484 | h264Frame.startNalUnit(); | 491 | h264Frame.startNalUnit(); |
485 | state = 2; // We know there will not be an overlapping start code, so we can skip that test | 492 | // We know there will not be an overlapping start code, so we can skip |
493 | // that test | ||
494 | state = 2; | ||
486 | return this.writeBytes(data, offset, length); | 495 | return this.writeBytes(data, offset, length); |
487 | /*--------------------------------------------------------------------------------------------------------------------*/ | ||
488 | } // switch | 496 | } // switch |
489 | }; | 497 | }; |
490 | }; | 498 | }; | ... | ... |
... | @@ -39,7 +39,7 @@ | ... | @@ -39,7 +39,7 @@ |
39 | duration = duration || 0; | 39 | duration = duration || 0; |
40 | audio = audio === undefined? true : audio; | 40 | audio = audio === undefined? true : audio; |
41 | video = video === undefined? true : video; | 41 | video = video === undefined? true : video; |
42 | 42 | ||
43 | // signature | 43 | // signature |
44 | head.setUint8(0, 0x46); // 'F' | 44 | head.setUint8(0, 0x46); // 'F' |
45 | head.setUint8(1, 0x4c); // 'L' | 45 | head.setUint8(1, 0x4c); // 'L' |
... | @@ -47,7 +47,7 @@ | ... | @@ -47,7 +47,7 @@ |
47 | 47 | ||
48 | // version | 48 | // version |
49 | head.setUint8(3, 0x01); | 49 | head.setUint8(3, 0x01); |
50 | 50 | ||
51 | // flags | 51 | // flags |
52 | head.setUint8(4, (audio ? 0x04 : 0x00) | (video ? 0x01 : 0x00)); | 52 | head.setUint8(4, (audio ? 0x04 : 0x00) | (video ? 0x01 : 0x00)); |
53 | 53 | ||
... | @@ -74,7 +74,7 @@ | ... | @@ -74,7 +74,7 @@ |
74 | 74 | ||
75 | return result; | 75 | return result; |
76 | }; | 76 | }; |
77 | 77 | ||
78 | self.flushTags = function() { | 78 | self.flushTags = function() { |
79 | h264Stream.finishFrame(); | 79 | h264Stream.finishFrame(); |
80 | }; | 80 | }; |
... | @@ -161,7 +161,7 @@ | ... | @@ -161,7 +161,7 @@ |
161 | streamBuffer.set(dataSlice, streamBufferByteCount); | 161 | streamBuffer.set(dataSlice, streamBufferByteCount); |
162 | 162 | ||
163 | parseTSPacket(streamBuffer); | 163 | parseTSPacket(streamBuffer); |
164 | 164 | ||
165 | // reset the buffer | 165 | // reset the buffer |
166 | streamBuffer = new Uint8Array(m2tsPacketSize); | 166 | streamBuffer = new Uint8Array(m2tsPacketSize); |
167 | streamBufferByteCount = 0; | 167 | streamBufferByteCount = 0; |
... | @@ -174,7 +174,7 @@ | ... | @@ -174,7 +174,7 @@ |
174 | // If there is no sync byte skip forward until we find one | 174 | // If there is no sync byte skip forward until we find one |
175 | // TODO if we find a sync byte, look 188 bytes in the future (if | 175 | // TODO if we find a sync byte, look 188 bytes in the future (if |
176 | // possible). If there is not a sync byte there, keep looking | 176 | // possible). If there is not a sync byte there, keep looking |
177 | dataPosition++; | 177 | dataPosition++; |
178 | } | 178 | } |
179 | 179 | ||
180 | // base case: not enough data to parse a m2ts packet | 180 | // base case: not enough data to parse a m2ts packet |
... | @@ -205,19 +205,18 @@ | ... | @@ -205,19 +205,18 @@ |
205 | // packet! | 205 | // packet! |
206 | parseTSPacket = function(data) { // :ByteArray):Boolean { | 206 | parseTSPacket = function(data) { // :ByteArray):Boolean { |
207 | var | 207 | var |
208 | s = 0, //:uint | 208 | offset = 0, // :uint |
209 | o = s, // :uint | 209 | end = offset + m2tsPacketSize, // :uint |
210 | e = o + m2tsPacketSize, // :uint | 210 | |
211 | |||
212 | // Don't look for a sync byte. We handle that in | 211 | // Don't look for a sync byte. We handle that in |
213 | // parseSegmentBinaryData() | 212 | // parseSegmentBinaryData() |
214 | 213 | ||
215 | // Payload Unit Start Indicator | 214 | // Payload Unit Start Indicator |
216 | pusi = !!(data[o + 1] & 0x40), // mask: 0100 0000 | 215 | pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000 |
217 | 216 | ||
218 | // PacketId | 217 | // PacketId |
219 | pid = (data[o + 1] & 0x1F) << 8 | data[o + 2], // mask: 0001 1111 | 218 | pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111 |
220 | afflag = (data[o + 3] & 0x30 ) >>> 4, | 219 | afflag = (data[offset + 3] & 0x30 ) >>> 4, |
221 | 220 | ||
222 | aflen, // :uint | 221 | aflen, // :uint |
223 | patTableId, // :int | 222 | patTableId, // :int |
... | @@ -242,29 +241,29 @@ | ... | @@ -242,29 +241,29 @@ |
242 | 241 | ||
243 | // Continuity Counter we could use this for sanity check, and | 242 | // Continuity Counter we could use this for sanity check, and |
244 | // corrupt stream detection | 243 | // corrupt stream detection |
245 | // cc = (data[o + 3] & 0x0F); | 244 | // cc = (data[offset + 3] & 0x0F); |
245 | |||
246 | // Done with TS header | ||
247 | offset += 4; | ||
246 | 248 | ||
247 | // Done with TS header | ||
248 | o += 4; | ||
249 | |||
250 | if (afflag > 0x01) { // skip most of the adaption field | 249 | if (afflag > 0x01) { // skip most of the adaption field |
251 | aflen = data[o]; | 250 | aflen = data[offset]; |
252 | o += aflen + 1; | 251 | offset += aflen + 1; |
253 | } | 252 | } |
254 | 253 | ||
255 | if (0x0000 === pid) { | 254 | if (0x0000 === pid) { |
256 | // always test for PMT first! (becuse other variables default to 0) | 255 | // always test for PMT first! (becuse other variables default to 0) |
257 | 256 | ||
258 | // if pusi is set we must skip X bytes (PSI pointer field) | 257 | // if pusi is set we must skip X bytes (PSI pointer field) |
259 | o += pusi ? 1 + data[o] : 0; | 258 | offset += pusi ? 1 + data[offset] : 0; |
260 | patTableId = data[o]; | 259 | patTableId = data[offset]; |
261 | 260 | ||
262 | console.assert(0x00 === patTableId, 'patTableId should be 0x00'); | 261 | console.assert(0x00 === patTableId, 'patTableId should be 0x00'); |
263 | 262 | ||
264 | patCurrentNextIndicator = !!(data[o + 5] & 0x01); | 263 | patCurrentNextIndicator = !!(data[offset + 5] & 0x01); |
265 | if (patCurrentNextIndicator) { | 264 | if (patCurrentNextIndicator) { |
266 | patSectionLength = (data[o + 1] & 0x0F) << 8 | data[o + 2]; | 265 | patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2]; |
267 | o += 8; // skip past PSI header | 266 | offset += 8; // skip past PSI header |
268 | 267 | ||
269 | // We currently only support streams with 1 program | 268 | // We currently only support streams with 1 program |
270 | patSectionLength = (patSectionLength - 9) / 4; | 269 | patSectionLength = (patSectionLength - 9) / 4; |
... | @@ -273,26 +272,26 @@ | ... | @@ -273,26 +272,26 @@ |
273 | } | 272 | } |
274 | 273 | ||
275 | // if we ever support more that 1 program (unlikely) loop over them here | 274 | // if we ever support more that 1 program (unlikely) loop over them here |
276 | // var programNumber = data[o + 0] << 8 | data[o + 1]; | 275 | // var programNumber = data[offset + 0] << 8 | data[offset + 1]; |
277 | // var programId = (data[o+2] & 0x1F) << 8 | data[o + 3]; | 276 | // var programId = (data[offset+2] & 0x1F) << 8 | data[offset + 3]; |
278 | pmtPid = (data[o + 2] & 0x1F) << 8 | data[o + 3]; | 277 | pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3]; |
279 | } | 278 | } |
280 | 279 | ||
281 | // We could test the CRC here to detect corruption with extra CPU cost | 280 | // We could test the CRC here to detect corruption with extra CPU cost |
282 | } else if (videoPid === pid || audioPid === pid) { | 281 | } else if (videoPid === pid || audioPid === pid) { |
283 | if (pusi) { | 282 | if (pusi) { |
284 | // comment out for speed | 283 | // comment out for speed |
285 | if (0x00 !== data[o + 0] || 0x00 !== data[o + 1] || 0x01 !== data[o + 2]) { | 284 | if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) { |
286 | // look for PES start code | 285 | // look for PES start code |
287 | throw new Error("PES did not begin with start code"); | 286 | throw new Error("PES did not begin with start code"); |
288 | } | 287 | } |
289 | 288 | ||
290 | // var sid:int = data[o+3]; // StreamID | 289 | // var sid:int = data[offset+3]; // StreamID |
291 | pesPacketSize = (data[o + 4] << 8) | data[o + 5]; | 290 | pesPacketSize = (data[offset + 4] << 8) | data[offset + 5]; |
292 | dataAlignmentIndicator = !!((data[o + 6] & 0x04) >>> 2); | 291 | dataAlignmentIndicator = (data[offset + 6] & 0x04) !== 0; |
293 | ptsDtsIndicator = (data[o + 7] & 0xC0) >>> 6; | 292 | ptsDtsIndicator = data[offset + 7]; |
294 | pesHeaderLength = data[o + 8]; // TODO sanity check header length | 293 | pesHeaderLength = data[offset + 8]; // TODO sanity check header length |
295 | o += 9; // Skip past PES header | 294 | offset += 9; // Skip past PES header |
296 | 295 | ||
297 | // PTS and DTS are normially stored as a 33 bit number. | 296 | // PTS and DTS are normially stored as a 33 bit number. |
298 | // JavaScript does not have a integer type larger than 32 bit | 297 | // JavaScript does not have a integer type larger than 32 bit |
... | @@ -300,26 +299,28 @@ | ... | @@ -300,26 +299,28 @@ |
300 | // so what we are going to do instead, is drop the least | 299 | // so what we are going to do instead, is drop the least |
301 | // significant bit (the same as dividing by two) then we can | 300 | // significant bit (the same as dividing by two) then we can |
302 | // divide by 45 (45 * 2 = 90) to get ms. | 301 | // divide by 45 (45 * 2 = 90) to get ms. |
303 | if (ptsDtsIndicator & 0x03) { | 302 | if (ptsDtsIndicator & 0xC0) { |
304 | pts = (data[o + 0] & 0x0E) << 28 | 303 | // the PTS and DTS are not written out directly. For information on |
305 | | (data[o + 1] & 0xFF) << 21 | 304 | // how they are encoded, see |
306 | | (data[o + 2] & 0xFE) << 13 | 305 | // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html |
307 | | (data[o + 3] & 0xFF) << 6 | 306 | pts = (data[offset + 0] & 0x0E) << 28 |
308 | | (data[o + 4] & 0xFE) >>> 2; | 307 | | (data[offset + 1] & 0xFF) << 21 |
308 | | (data[offset + 2] & 0xFE) << 13 | ||
309 | | (data[offset + 3] & 0xFF) << 6 | ||
310 | | (data[offset + 4] & 0xFE) >>> 2; | ||
309 | pts /= 45; | 311 | pts /= 45; |
310 | if (ptsDtsIndicator & 0x01) {// DTS | 312 | dts = pts; |
311 | dts = (data[o + 5] & 0x0E ) << 28 | 313 | if (ptsDtsIndicator & 0x40) {// DTS |
312 | | (data[o + 6] & 0xFF ) << 21 | 314 | dts = (data[offset + 5] & 0x0E ) << 28 |
313 | | (data[o + 7] & 0xFE ) << 13 | 315 | | (data[offset + 6] & 0xFF ) << 21 |
314 | | (data[o + 8] & 0xFF ) << 6 | 316 | | (data[offset + 7] & 0xFE ) << 13 |
315 | | (data[o + 9] & 0xFE ) >>> 2; | 317 | | (data[offset + 8] & 0xFF ) << 6 |
318 | | (data[offset + 9] & 0xFE ) >>> 2; | ||
316 | dts /= 45; | 319 | dts /= 45; |
317 | } else { | ||
318 | dts = pts; | ||
319 | } | 320 | } |
320 | } | 321 | } |
321 | // Skip past "optional" portion of PTS header | 322 | // Skip past "optional" portion of PTS header |
322 | o += pesHeaderLength; | 323 | offset += pesHeaderLength; |
323 | 324 | ||
324 | if (videoPid === pid) { | 325 | if (videoPid === pid) { |
325 | // Stash this frame for future use. | 326 | // Stash this frame for future use. |
... | @@ -336,31 +337,31 @@ | ... | @@ -336,31 +337,31 @@ |
336 | } | 337 | } |
337 | 338 | ||
338 | if (audioPid === pid) { | 339 | if (audioPid === pid) { |
339 | aacStream.writeBytes(data, o, e - o); | 340 | aacStream.writeBytes(data, offset, end - offset); |
340 | } else if (videoPid === pid) { | 341 | } else if (videoPid === pid) { |
341 | h264Stream.writeBytes(data, o, e - o); | 342 | h264Stream.writeBytes(data, offset, end - offset); |
342 | } | 343 | } |
343 | } else if (pmtPid === pid) { | 344 | } else if (pmtPid === pid) { |
344 | // TODO sanity check data[o] | 345 | // TODO sanity check data[offset] |
345 | // if pusi is set we must skip X bytes (PSI pointer field) | 346 | // if pusi is set we must skip X bytes (PSI pointer field) |
346 | o += (pusi ? 1 + data[o] : 0); | 347 | offset += (pusi ? 1 + data[offset] : 0); |
347 | pmtTableId = data[o]; | 348 | pmtTableId = data[offset]; |
348 | 349 | ||
349 | console.assert(0x02 === pmtTableId); | 350 | console.assert(0x02 === pmtTableId); |
350 | 351 | ||
351 | pmtCurrentNextIndicator = !!(data[o + 5] & 0x01); | 352 | pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01); |
352 | if (pmtCurrentNextIndicator) { | 353 | if (pmtCurrentNextIndicator) { |
353 | audioPid = videoPid = 0; | 354 | audioPid = videoPid = 0; |
354 | pmtSectionLength = (data[o + 1] & 0x0F) << 8 | data[o + 2]; | 355 | pmtSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2]; |
355 | // skip CRC and PSI data we dont care about | 356 | // skip CRC and PSI data we dont care about |
356 | pmtSectionLength -= 13; | 357 | pmtSectionLength -= 13; |
357 | 358 | ||
358 | o += 12; // skip past PSI header and some PMT data | 359 | offset += 12; // skip past PSI header and some PMT data |
359 | while (0 < pmtSectionLength) { | 360 | while (0 < pmtSectionLength) { |
360 | streamType = data[o + 0]; | 361 | streamType = data[offset + 0]; |
361 | elementaryPID = (data[o + 1] & 0x1F) << 8 | data[o + 2]; | 362 | elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2]; |
362 | ESInfolength = (data[o + 3] & 0x0F) << 8 | data[o + 4]; | 363 | ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4]; |
363 | o += 5 + ESInfolength; | 364 | offset += 5 + ESInfolength; |
364 | pmtSectionLength -= 5 + ESInfolength; | 365 | pmtSectionLength -= 5 + ESInfolength; |
365 | 366 | ||
366 | if (0x1B === streamType) { | 367 | if (0x1B === streamType) { |
... | @@ -398,4 +399,4 @@ | ... | @@ -398,4 +399,4 @@ |
398 | } | 399 | } |
399 | }; | 400 | }; |
400 | }; | 401 | }; |
401 | })(this); | 402 | })(window); | ... | ... |
... | @@ -44,4 +44,17 @@ test('writeShort writes a two byte sequence', function() { | ... | @@ -44,4 +44,17 @@ test('writeShort writes a two byte sequence', function() { |
44 | 'the value is written'); | 44 | 'the value is written'); |
45 | }); | 45 | }); |
46 | 46 | ||
47 | test('writeBytes grows the internal byte array dynamically', function() { | ||
48 | var | ||
49 | tag = new FlvTag(FlvTag.VIDEO_TAG), | ||
50 | tooManyBytes = new Uint8Array(tag.bytes.byteLength + 1); | ||
51 | |||
52 | try { | ||
53 | tag.writeBytes(tooManyBytes); | ||
54 | ok(true, 'the buffer grew to fit the data'); | ||
55 | } catch(e) { | ||
56 | ok(!e, 'the buffer should grow'); | ||
57 | } | ||
58 | }); | ||
59 | |||
47 | })(this); | 60 | })(this); | ... | ... |
test/perf.html
0 → 100644
1 | <!doctype html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <title>MPEG-TS Parser Performance Workbench</title> | ||
5 | <!-- video.js --> | ||
6 | <script src="../node_modules/video.js/video.dev.js"></script> | ||
7 | |||
8 | <!-- HLS plugin --> | ||
9 | <script src="../src/video-js-hls.js"></script> | ||
10 | <script src="../src/flv-tag.js"></script> | ||
11 | <script src="../src/exp-golomb.js"></script> | ||
12 | <script src="../src/h264-stream.js"></script> | ||
13 | <script src="../src/aac-stream.js"></script> | ||
14 | <script src="../src/segment-parser.js"></script> | ||
15 | |||
16 | <!-- MPEG-TS segment --> | ||
17 | <script src="tsSegment-bc.js"></script> | ||
18 | <style> | ||
19 | .desc { | ||
20 | background-color: #ddd; | ||
21 | border: thin solid #333; | ||
22 | padding: 8px; | ||
23 | } | ||
24 | </style> | ||
25 | </head> | ||
26 | <body> | ||
27 | <p class="desc">Select your number of iterations and then press "Run" to begin parsing MPEG-TS packets into FLV tags. This page can be handy for identifying segment parser performance bottlenecks.</p> | ||
28 | <form> | ||
29 | <input name="iterations" min="1" type="number" value="1"> | ||
30 | <button type="sumbit">Run</button> | ||
31 | </form> | ||
32 | <table> | ||
33 | <thead> | ||
34 | <th>Iterations</th><th>Time</th><th>MB/second</th> | ||
35 | </thead> | ||
36 | <tbody class="results"></tbody> | ||
37 | </table> | ||
38 | <script> | ||
39 | var | ||
40 | button = document.querySelector('button'), | ||
41 | input = document.querySelector('input'), | ||
42 | results = document.querySelector('.results'), | ||
43 | |||
44 | reportResults = function(count, elapsed) { | ||
45 | var | ||
46 | row = document.createElement('tr'), | ||
47 | countCell = document.createElement('td'), | ||
48 | elapsedCell = document.createElement('td'), | ||
49 | throughputCell = document.createElement('td'); | ||
50 | |||
51 | countCell.innerText = count; | ||
52 | elapsedCell.innerText = elapsed; | ||
53 | throughputCell.innerText = (((bcSegment.byteLength * count * 1000) / elapsed) / (Math.pow(2, 20))).toFixed(3); | ||
54 | row.appendChild(countCell); | ||
55 | row.appendChild(elapsedCell); | ||
56 | row.appendChild(throughputCell); | ||
57 | |||
58 | results.insertBefore(row, results.firstChild); | ||
59 | }; | ||
60 | |||
61 | button.addEventListener('click', function(event) { | ||
62 | var | ||
63 | iterations = input.value, | ||
64 | parser = new window.videojs.hls.SegmentParser(), | ||
65 | start; | ||
66 | |||
67 | // setup | ||
68 | start = +new Date(); | ||
69 | |||
70 | while (iterations--) { | ||
71 | |||
72 | // parse the segment | ||
73 | parser.parseSegmentBinaryData(window.bcSegment); | ||
74 | |||
75 | // finalize all the FLV tags | ||
76 | while (parser.tagsAvailable()) { | ||
77 | parser.getNextTag(); | ||
78 | } | ||
79 | } | ||
80 | |||
81 | // report | ||
82 | reportResults(input.value, (+new Date()) - start); | ||
83 | |||
84 | // don't actually submit the form | ||
85 | event.preventDefault(); | ||
86 | }, false); | ||
87 | </script> | ||
88 | </body> | ||
89 | </html> |
-
Please register or sign in to post a comment