toms working example base
Showing
7 changed files
with
485 additions
and
374 deletions
1 | <!DOCTYPE html> | 1 | <!DOCTYPE html> |
2 | <html> | 2 | <html> |
3 | <head> | 3 | <head> |
4 | <meta charset="utf-8"> | 4 | <meta charset="utf-8"> |
5 | <title>video.js HLS Plugin Example</title> | 5 | <title>video.js HLS Plugin Example</title> |
6 | 6 | ||
7 | <link href="node_modules/video.js/video-js.css" rel="stylesheet"> | 7 | <link href="node_modules/video.js/video-js.css" rel="stylesheet"> |
8 | 8 | ||
... | @@ -25,9 +25,11 @@ | ... | @@ -25,9 +25,11 @@ |
25 | 25 | ||
26 | <!-- m3u8 handling --> | 26 | <!-- m3u8 handling --> |
27 | <script src="src/m3u8/m3u8.js"></script> | 27 | <script src="src/m3u8/m3u8.js"></script> |
28 | <script src="src/m3u8/m3u8-parser.js"></script> | ||
29 | <script src="src/m3u8/m3u8-tag-types.js"></script> | 28 | <script src="src/m3u8/m3u8-tag-types.js"></script> |
29 | <script src="src/m3u8/m3u8-parser.js"></script> | ||
30 | <script src="src/manifest-controller.js"></script> | 30 | <script src="src/manifest-controller.js"></script> |
31 | <script src="src/segment-controller.js"></script> | ||
32 | <script src="src/hls-playback-controller.js"></script> | ||
31 | 33 | ||
32 | <!-- example MPEG2-TS segments --> | 34 | <!-- example MPEG2-TS segments --> |
33 | <!-- bipbop --> | 35 | <!-- bipbop --> |
... | @@ -48,51 +50,12 @@ | ... | @@ -48,51 +50,12 @@ |
48 | 50 | ||
49 | // initialize the player | 51 | // initialize the player |
50 | 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'; |
51 | video = videojs('video'); | 53 | video = videojs('video',{},function(){ |
52 | 54 | this.playbackController = new window.videojs.hls.HLSPlaybackController(this); | |
53 | // create a media source | 55 | this.playbackController.loadManifest("http://localhost:7070/test/basic-playback/zencoder/gogo/manifest.m3u8",function(data){console.log(data)}); |
54 | mediaSource = new videojs.MediaSource(); | ||
55 | mediaSource.addEventListener('sourceopen', function(event){ | ||
56 | var | ||
57 | parser = new videojs.hls.SegmentParser(), | ||
58 | sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'); | ||
59 | everything = []; | ||
60 | |||
61 | // feed parsed bytes into the player | ||
62 | everything.push(parser.getFlvHeader()); | ||
63 | sourceBuffer.appendBuffer(everything[everything.length - 1], video); | ||
64 | |||
65 | parser.parseSegmentBinaryData(window.bcSegment); | ||
66 | |||
67 | while (parser.tagsAvailable()) { | ||
68 | everything.push(parser.getNextTag().bytes); | ||
69 | sourceBuffer.appendBuffer(everything[everything.length - 1], video); | ||
70 | } | ||
71 | parser.flushTags(); | ||
72 | while (parser.tagsAvailable()) { | ||
73 | everything.push(parser.getNextTag().bytes); | ||
74 | sourceBuffer.appendBuffer(everything[everything.length - 1], video); | ||
75 | } | ||
76 | |||
77 | var iframe = document.createElement('iframe'); | ||
78 | iframe.src = 'data:video/x-flv;base64,' + window.btoa((Array.prototype.map.call(everything.reduce(function(result, next) { | ||
79 | var array = new Uint8Array(result.byteLength + next.byteLength); | ||
80 | array.set(result); | ||
81 | array.set(next, result.length); | ||
82 | return array; | ||
83 | }), function(byte) { | ||
84 | return String.fromCharCode(byte); | ||
85 | })).join('')); | ||
86 | //console.log(iframe); | ||
87 | // document.body.appendChild(iframe); | ||
88 | }, false); | ||
89 | |||
90 | url = videojs.URL.createObjectURL(mediaSource); | ||
91 | |||
92 | video.src({ | ||
93 | src: url, | ||
94 | type: "video/flv" | ||
95 | }); | 56 | }); |
96 | </script> | 57 | </script> |
58 | |||
59 | |||
97 | </body> | 60 | </body> |
98 | </html> | 61 | </html> | ... | ... |
src/hls-playback-controller.js
0 → 100644
1 | (function (window) { | ||
2 | window.videojs.hls.HLSPlaybackController = function (vjsPlayerReference) { | ||
3 | var ManifestController = window.videojs.hls.ManifestController; | ||
4 | var SegmentController = window.videojs.hls.SegmentController; | ||
5 | var MediaSource = window.videojs.MediaSource; | ||
6 | var SegmentParser = window.videojs.hls.SegmentParser; | ||
7 | var M3U8 = window.videojs.hls.M3U8; | ||
8 | |||
9 | var self = this; | ||
10 | |||
11 | self.player = vjsPlayerReference; | ||
12 | self.mediaSource = new MediaSource(); | ||
13 | self.parser = new SegmentParser(); | ||
14 | |||
15 | self.manifestController = null; | ||
16 | self.segmentController = null; | ||
17 | self.manifestLoaded = false; | ||
18 | self.currentSegment = 0; | ||
19 | self.currentManifest = null; | ||
20 | self.currentPlaylist = null; | ||
21 | self.currentRendition = null; | ||
22 | |||
23 | // Register Externall Callbacks | ||
24 | self.manifestLoadCompleteCallback; | ||
25 | |||
26 | self.player.on('timeupdate', function () { | ||
27 | console.log(self.player.currentTime()); | ||
28 | }); | ||
29 | |||
30 | self.player.on('onsrcchange', function () { | ||
31 | console.log('src change', self.player.currentSrc()); | ||
32 | //if src.url.m3u8 -- loadManifest.url | ||
33 | }); | ||
34 | |||
35 | self.rendition = function (rendition) { | ||
36 | self.currentRendition = rendition; | ||
37 | self.loadManifest(self.currentRendition.url, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update); | ||
38 | }; | ||
39 | |||
40 | self.loadManifest = function (manifestUrl, onDataCallback, onErrorCallback, onUpdateCallback) { | ||
41 | self.mediaSource.addEventListener('sourceopen', function (event) { | ||
42 | console.log('source open here'); | ||
43 | // feed parsed bytes into the player | ||
44 | self.sourceBuffer = self.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'); | ||
45 | |||
46 | self.parser = new SegmentParser(); | ||
47 | |||
48 | self.sourceBuffer.appendBuffer(self.parser.getFlvHeader(), video); | ||
49 | |||
50 | if( onDataCallback ) | ||
51 | { | ||
52 | self.manifestLoadCompleteCallback = onDataCallback; | ||
53 | } | ||
54 | |||
55 | self.manifestController = new ManifestController(); | ||
56 | self.manifestController.loadManifest(manifestUrl, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update); | ||
57 | |||
58 | }, false); | ||
59 | |||
60 | self.player.src({ | ||
61 | src: videojs.URL.createObjectURL(self.mediaSource), | ||
62 | type: "video/flv" | ||
63 | }); | ||
64 | }; | ||
65 | |||
66 | self.onM3U8LoadComplete = function (m3u8) { | ||
67 | if (m3u8.invalidReasons.length == 0) { | ||
68 | if(m3u8.isPlaylist) | ||
69 | { | ||
70 | self.currentPlaylist = m3u8; | ||
71 | self.rendition(self.currentPlaylist.playlistItems[0]); | ||
72 | } else { | ||
73 | self.currentManifest = m3u8; | ||
74 | self.manifestLoaded = true; | ||
75 | |||
76 | self.loadSegment(self.currentManifest.mediaItems[0]); | ||
77 | |||
78 | if(self.manifestLoadCompleteCallback) | ||
79 | { | ||
80 | self.manifestLoadCompleteCallback(m3u8); | ||
81 | } | ||
82 | } | ||
83 | } | ||
84 | }; | ||
85 | |||
86 | self.onM3U8LoadError = function (error) { | ||
87 | |||
88 | }; | ||
89 | |||
90 | self.onM3U8Update = function (m3u8) { | ||
91 | |||
92 | }; | ||
93 | |||
94 | self.loadSegment = function(segment) { | ||
95 | self.segmentController = new SegmentController(); | ||
96 | self.segmentController.loadSegment(segment.url, self.onSegmentLoadComplete, self.onSegmentLoadError); | ||
97 | |||
98 | }; | ||
99 | |||
100 | self.onSegmentLoadComplete = function (segment) { | ||
101 | self.parser.parseSegmentBinaryData(segment.binaryData); | ||
102 | |||
103 | while (self.parser.tagsAvailable()) { | ||
104 | self.sourceBuffer.appendBuffer(self.parser.getNextTag().bytes, self.player); | ||
105 | }; | ||
106 | |||
107 | console.log('load another',self.currentSegment,self.currentManifest.mediaItems.length); | ||
108 | |||
109 | if(self.currentSegment < self.currentManifest.mediaItems.length-1) | ||
110 | { | ||
111 | console.log('load another'); | ||
112 | self.loadNextSegment(); | ||
113 | } | ||
114 | }; | ||
115 | |||
116 | self.loadNextSegment = function () { | ||
117 | self.currentSegment++; | ||
118 | self.loadSegment(self.currentManifest.mediaItems[self.currentSegment]); | ||
119 | } | ||
120 | |||
121 | self.onSegmentLoadError = function (error) { | ||
122 | |||
123 | }; | ||
124 | |||
125 | }; | ||
126 | })(this); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
... | @@ -4,6 +4,8 @@ | ... | @@ -4,6 +4,8 @@ |
4 | window.videojs.hls.M3U8Parser = function() { | 4 | window.videojs.hls.M3U8Parser = function() { |
5 | 5 | ||
6 | var self = this; | 6 | var self = this; |
7 | self.directory; | ||
8 | |||
7 | var tagTypes = window.videojs.hls.m3u8TagType; | 9 | var tagTypes = window.videojs.hls.m3u8TagType; |
8 | var lines = []; | 10 | var lines = []; |
9 | var data; | 11 | var data; |
... | @@ -31,6 +33,11 @@ | ... | @@ -31,6 +33,11 @@ |
31 | self.parse = function( rawDataString ) { | 33 | self.parse = function( rawDataString ) { |
32 | data = new M3U8(); | 34 | data = new M3U8(); |
33 | 35 | ||
36 | if(self.directory) | ||
37 | { | ||
38 | data.directory = self.directory; | ||
39 | } | ||
40 | |||
34 | if( rawDataString != undefined && rawDataString.toString().length > 0 ) | 41 | if( rawDataString != undefined && rawDataString.toString().length > 0 ) |
35 | { | 42 | { |
36 | lines = rawDataString.split('\n'); | 43 | lines = rawDataString.split('\n'); |
... | @@ -72,6 +79,15 @@ | ... | @@ -72,6 +79,15 @@ |
72 | segment.url = lines[index+1]; | 79 | segment.url = lines[index+1]; |
73 | } | 80 | } |
74 | 81 | ||
82 | if(segment.url.indexOf("http")===-1 && self.directory) | ||
83 | { | ||
84 | if(data.directory[data.directory.length-1] === segment.url[0] && segment.url[0] === "/") | ||
85 | { | ||
86 | segment.url = segment.url.substr(1); | ||
87 | } | ||
88 | segment.url = self.directory + segment.url; | ||
89 | } | ||
90 | |||
75 | data.mediaItems.push(segment); | 91 | data.mediaItems.push(segment); |
76 | 92 | ||
77 | break; | 93 | break; |
... | @@ -116,7 +132,7 @@ | ... | @@ -116,7 +132,7 @@ |
116 | break; | 132 | break; |
117 | 133 | ||
118 | case tagTypes.ZEN_TOTAL_DURATION: | 134 | case tagTypes.ZEN_TOTAL_DURATION: |
119 | data.totalDuration = self.getTagValue(value); | 135 | data.totalDuration = Number(self.getTagValue(value)); |
120 | break; | 136 | break; |
121 | 137 | ||
122 | case tagTypes.VERSION: | 138 | case tagTypes.VERSION: | ... | ... |
1 | (function(window) { | 1 | (function(window) { |
2 | window.videojs.hls.M3U8 = function() { | 2 | window.videojs.hls.M3U8 = function() { |
3 | this.directory = ""; | ||
3 | this.allowCache = "NO"; | 4 | this.allowCache = "NO"; |
4 | this.playlistItems = []; | 5 | this.playlistItems = []; |
5 | this.mediaItems = []; | 6 | this.mediaItems = []; | ... | ... |
1 | (function(window) { | 1 | (function (window) { |
2 | var M3U8 = window.videojs.hls.M3U8; | 2 | var M3U8 = window.videojs.hls.M3U8; |
3 | var M3U8Parser = window.videojs.hls.M3U8Parser; | 3 | var M3U8Parser = window.videojs.hls.M3U8Parser; |
4 | 4 | ||
5 | window.videojs.hls.ManifestController = function(){ | 5 | window.videojs.hls.ManifestController = function () { |
6 | |||
7 | var self = this; | 6 | var self = this; |
8 | var parser; | ||
9 | var data; | ||
10 | 7 | ||
11 | var onDataCallback; | 8 | self.parser; |
12 | var onErrorCallback; | 9 | self.data; |
13 | var onUpdateCallback; | 10 | self.url; |
11 | |||
12 | self.onDataCallback; | ||
13 | self.onErrorCallback; | ||
14 | self.onUpdateCallback; | ||
15 | |||
16 | self.loadManifest = function (manifestUrl, onDataCallback, onErrorCallback, onUpdateCallback) { | ||
17 | self.url = manifestUrl; | ||
14 | 18 | ||
15 | self.loadManifest = function ( manifestUrl, onDataCallback, onErrorCallback, onUpdateCallback ) { | 19 | if (onDataCallback) { |
16 | self.onDataCallback = onDataCallback; | 20 | self.onDataCallback = onDataCallback; |
17 | self.onErrorCallback = onErrorCallback; | 21 | } |
18 | self.onUpdateCallback = onUpdateCallback; | 22 | if (onErrorCallback) { |
23 | self.onErrorCallback = onErrorCallback; | ||
24 | } | ||
25 | |||
26 | if (onUpdateCallback) { | ||
27 | self.onUpdateCallback = onUpdateCallback; | ||
28 | } | ||
19 | 29 | ||
20 | vjs.get(manifestUrl, self.onManifestLoadComplete, self.onManifestLoadError); | 30 | vjs.get(manifestUrl, self.onManifestLoadComplete, self.onManifestLoadError); |
21 | }; | 31 | }; |
22 | 32 | ||
23 | self.parseManifest = function ( dataAsString ) { | 33 | self.parseManifest = function (dataAsString) { |
24 | self.parser = new M3U8Parser(); | 34 | self.parser = new M3U8Parser(); |
25 | self.data = self.parser.parse( dataAsString ); | 35 | self.parser.directory = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(self.url).slice(1)[1]; |
36 | self.data = self.parser.parse(dataAsString); | ||
26 | 37 | ||
27 | return self.data; | 38 | return self.data; |
28 | }; | 39 | }; |
29 | 40 | ||
30 | self.onManifestLoadComplete = function(response) { | 41 | self.onManifestLoadComplete = function (response) { |
31 | var output = self.parseManifest(response); | 42 | var output = self.parseManifest(response); |
32 | 43 | ||
33 | if(self.onDataCallback != undefined) | 44 | if (self.onDataCallback != undefined) { |
34 | { | ||
35 | self.onDataCallback(output); | 45 | self.onDataCallback(output); |
36 | } | 46 | } |
37 | }; | 47 | }; |
38 | 48 | ||
39 | self.onManifestLoadError = function(err) { | 49 | self.onManifestLoadError = function (err) { |
40 | if(err) | 50 | if (self.onErrorCallback != undefined) { |
41 | { | 51 | self.onErrorCallback((err != undefined) ? err : null); |
42 | console.log(err.message); | ||
43 | } | ||
44 | |||
45 | if(self.onErrorCallback != undefined) | ||
46 | { | ||
47 | onErrorCallback((err != undefined) ? err : null); | ||
48 | } | 52 | } |
49 | }; | 53 | }; |
50 | } | 54 | } | ... | ... |
1 | (function(window) { | 1 | (function(window) { |
2 | 2 | ||
3 | var SegmentParser = window.videojs.hls.SegmentParser; | ||
4 | |||
5 | window.videojs.hls.SegmentController = function(){ | 3 | window.videojs.hls.SegmentController = function(){ |
6 | 4 | ||
7 | var self = this; | 5 | var self = this; |
8 | var url; | ||
9 | var parser; | ||
10 | var requestTimestamp; | ||
11 | var responseTimestamp; | ||
12 | var data; | ||
13 | 6 | ||
14 | var onDataCallback; | 7 | self.url; |
15 | var onErrorCallback; | 8 | |
16 | var onUpdateCallback; | 9 | self.requestTimestamp; |
10 | self.responseTimestamp; | ||
11 | self.data; | ||
12 | |||
13 | self.onDataCallback; | ||
14 | self.onErrorCallback; | ||
15 | self.onUpdateCallback; | ||
17 | 16 | ||
18 | self.loadSegment = function ( segmentUrl, onDataCallback, onErrorCallback, onUpdateCallback ) { | 17 | self.loadSegment = function ( segmentUrl, onDataCallback, onErrorCallback, onUpdateCallback ) { |
19 | self.url = segmentUrl; | 18 | self.url = segmentUrl; |
... | @@ -22,29 +21,31 @@ | ... | @@ -22,29 +21,31 @@ |
22 | self.onUpdateCallback = onUpdateCallback; | 21 | self.onUpdateCallback = onUpdateCallback; |
23 | self.requestTimestamp = new Date().getTime(); | 22 | self.requestTimestamp = new Date().getTime(); |
24 | 23 | ||
25 | var req = new XMLHttpRequest(); | 24 | var req = new XMLHttpRequest(); |
26 | req.open('GET', segmentUrl, true); | 25 | req.open('GET', segmentUrl, true); |
27 | req.responseType = 'arraybuffer'; | 26 | req.responseType = 'arraybuffer'; |
28 | req.onload = function(response) { | 27 | req.onload = function(response) { |
29 | self.onSegmentLoadComplete(new Uint8Array(req.response)); | 28 | self.onSegmentLoadComplete(new Uint8Array(req.response)); |
30 | }; | 29 | }; |
31 | 30 | ||
32 | req.send(null); | 31 | req.send(null); |
32 | |||
33 | //vjs.get(segmentUrl, self.onSegmentLoadComplete, self.onSegmentLoadError); | ||
33 | }; | 34 | }; |
34 | 35 | ||
35 | self.parseSegment = function ( incomingData ) { | 36 | self.parseSegment = function ( incomingData ) { |
36 | // Add David's code later // | 37 | // Add David's code later // |
38 | console.log('got segment data', incomingData.byteLength); | ||
37 | 39 | ||
38 | self.data = { | 40 | self.data = {}; |
39 | whatever: incomingData | 41 | self.data.binaryData = incomingData; |
40 | }; | ||
41 | self.data.url = self.url; | 42 | self.data.url = self.url; |
42 | self.data.isCached = false; | 43 | self.data.isCached = false; |
43 | self.data.requestTimestamp = self.requestTimestamp; | 44 | self.data.requestTimestamp = self.requestTimestamp; |
44 | self.data.responseTimestamp = self.responseTimestamp; | 45 | self.data.responseTimestamp = self.responseTimestamp; |
45 | self.data.byteLength = incomingData.byteLength; | 46 | self.data.byteLength = incomingData.byteLength; |
46 | self.data.isCached = ( parseInt(self.responseTimestamp - self.requestTimestamp) < 75 ); | 47 | self.data.isCached = ( parseInt(self.responseTimestamp - self.requestTimestamp) < 75 ); |
47 | self.data.throughput = self.calculateThroughput(self.data.byteLength, self.requestTimestamp ,self.responseTimestamp) | 48 | self.data.throughput = self.calculateThroughput(self.data.byteLength, self.requestTimestamp ,self.responseTimestamp); |
48 | 49 | ||
49 | return self.data; | 50 | return self.data; |
50 | }; | 51 | }; | ... | ... |
1 | (function(window) { | 1 | (function (window) { |
2 | /* | 2 | /* |
3 | ======== A Handy Little QUnit Reference ======== | 3 | ======== A Handy Little QUnit Reference ======== |
4 | http://api.qunitjs.com/ | 4 | http://api.qunitjs.com/ |
5 | 5 | ||
6 | Test methods: | 6 | Test methods: |
7 | module(name, {[setup][ ,teardown]}) | 7 | module(name, {[setup][ ,teardown]}) |
8 | test(name, callback) | 8 | test(name, callback) |
9 | expect(numberOfAssertions) | 9 | expect(numberOfAssertions) |
10 | stop(increment) | 10 | stop(increment) |
11 | start(decrement) | 11 | start(decrement) |
12 | Test assertions: | 12 | Test assertions: |
13 | ok(value, [message]) | 13 | ok(value, [message]) |
14 | equal(actual, expected, [message]) | 14 | equal(actual, expected, [message]) |
15 | notEqual(actual, expected, [message]) | 15 | notEqual(actual, expected, [message]) |
16 | deepEqual(actual, expected, [message]) | 16 | deepEqual(actual, expected, [message]) |
17 | notDeepEqual(actual, expected, [message]) | 17 | notDeepEqual(actual, expected, [message]) |
18 | strictEqual(actual, expected, [message]) | 18 | strictEqual(actual, expected, [message]) |
19 | notStrictEqual(actual, expected, [message]) | 19 | notStrictEqual(actual, expected, [message]) |
20 | throws(block, [expected], [message]) | 20 | throws(block, [expected], [message]) |
21 | */ | 21 | */ |
22 | var | 22 | var |
23 | manifestController, | 23 | manifestController, |
24 | segmentController, | 24 | segmentController, |
25 | m3u8parser, | 25 | m3u8parser, |
26 | parser, | 26 | parser, |
27 | 27 | ||
28 | expectedHeader = [ | 28 | expectedHeader = [ |
29 | 0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, | 29 | 0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, |
30 | 0x09, 0x00, 0x00, 0x00, 0x00 | 30 | 0x09, 0x00, 0x00, 0x00, 0x00 |
31 | ], | 31 | ], |
32 | testAudioTag, | 32 | testAudioTag, |
33 | testVideoTag, | 33 | testVideoTag, |
34 | testScriptTag, | 34 | testScriptTag, |
35 | asciiFromBytes, | 35 | asciiFromBytes, |
36 | testScriptString, | 36 | testScriptString, |
37 | testScriptEcmaArray; | 37 | testScriptEcmaArray; |
38 | 38 | ||
39 | module('environment'); | 39 | module('environment'); |
40 | 40 | ||
41 | test('is sane', function() { | 41 | test('is sane', function () { |
42 | expect(1); | 42 | expect(1); |
43 | ok(true); | 43 | ok(true); |
44 | }); | 44 | }); |
45 | 45 | ||
46 | module('segment parser', { | 46 | module('segment parser', { |
47 | setup: function() { | 47 | setup: function () { |
48 | parser = new window.videojs.hls.SegmentParser(); | 48 | parser = new window.videojs.hls.SegmentParser(); |
49 | } | 49 | } |
50 | }); | 50 | }); |
51 | 51 | ||
52 | test('creates an flv header', function() { | 52 | test('creates an flv header', function () { |
53 | var header = Array.prototype.slice.call(parser.getFlvHeader()); | 53 | var header = Array.prototype.slice.call(parser.getFlvHeader()); |
54 | ok(header, 'the header is truthy'); | 54 | ok(header, 'the header is truthy'); |
55 | equal(9 + 4, header.length, 'the header length is correct'); | 55 | equal(9 + 4, header.length, 'the header length is correct'); |
56 | equal(header[0], 'F'.charCodeAt(0), 'the first character is "F"'); | 56 | equal(header[0], 'F'.charCodeAt(0), 'the first character is "F"'); |
57 | equal(header[1], 'L'.charCodeAt(0), 'the second character is "L"'); | 57 | equal(header[1], 'L'.charCodeAt(0), 'the second character is "L"'); |
58 | equal(header[2], 'V'.charCodeAt(0), 'the third character is "V"'); | 58 | equal(header[2], 'V'.charCodeAt(0), 'the third character is "V"'); |
59 | 59 | ||
60 | deepEqual(expectedHeader, header, 'the rest of the header is correct'); | 60 | deepEqual(expectedHeader, header, 'the rest of the header is correct'); |
61 | }); | 61 | }); |
62 | 62 | ||
63 | test('parses the first bipbop segment', function() { | 63 | test('parses the first bipbop segment', function () { |
64 | var tag, bytes, i; | 64 | var tag, bytes, i; |
65 | parser.parseSegmentBinaryData(window.bcSegment); | 65 | parser.parseSegmentBinaryData(window.bcSegment); |
66 | 66 | ||
67 | ok(parser.tagsAvailable(), 'tags are available'); | 67 | ok(parser.tagsAvailable(), 'tags are available'); |
68 | 68 | ||
69 | console.log('h264 tags:', parser.stats.h264Tags(), | 69 | console.log('h264 tags:', parser.stats.h264Tags(), |
70 | 'aac tags:', parser.stats.aacTags()); | 70 | 'aac tags:', parser.stats.aacTags()); |
71 | }); | 71 | }); |
72 | 72 | ||
73 | testAudioTag = function(tag) { | 73 | testAudioTag = function (tag) { |
74 | var | 74 | var |
75 | byte = tag.bytes[11], | 75 | byte = tag.bytes[11], |
76 | format = (byte & 0xF0) >>> 4, | 76 | format = (byte & 0xF0) >>> 4, |
77 | soundRate = byte & 0x03, | 77 | soundRate = byte & 0x03, |
78 | soundSize = (byte & 0x2) >>> 1, | 78 | soundSize = (byte & 0x2) >>> 1, |
79 | soundType = byte & 0x1, | 79 | soundType = byte & 0x1, |
80 | aacPacketType = tag.bytes[12]; | 80 | aacPacketType = tag.bytes[12]; |
81 | 81 | ||
82 | equal(10, format, 'the audio format is aac'); | 82 | equal(10, format, 'the audio format is aac'); |
83 | equal(3, soundRate, 'the sound rate is 44kHhz'); | 83 | equal(3, soundRate, 'the sound rate is 44kHhz'); |
84 | equal(1, soundSize, 'the sound size is 16-bit samples'); | 84 | equal(1, soundSize, 'the sound size is 16-bit samples'); |
85 | equal(1, soundType, 'the sound type is stereo'); | 85 | equal(1, soundType, 'the sound type is stereo'); |
86 | 86 | ||
87 | ok(aacPacketType === 0 || aacPacketType === 1, 'aac packets should have a valid type'); | 87 | ok(aacPacketType === 0 || aacPacketType === 1, 'aac packets should have a valid type'); |
88 | }; | 88 | }; |
89 | 89 | ||
90 | testVideoTag = function(tag) { | 90 | testVideoTag = function (tag) { |
91 | var | 91 | var |
92 | byte = tag.bytes[11], | 92 | byte = tag.bytes[11], |
93 | frameType = (byte & 0xF0) >>> 4, | 93 | frameType = (byte & 0xF0) >>> 4, |
94 | codecId = byte & 0x0F, | 94 | codecId = byte & 0x0F, |
95 | packetType = tag.bytes[12], | 95 | packetType = tag.bytes[12], |
96 | compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8, | 96 | compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8, |
97 | nalHeader; | 97 | nalHeader; |
98 | 98 | ||
99 | // payload starts at tag.bytes[16] | 99 | // payload starts at tag.bytes[16] |
100 | 100 | ||
101 | 101 | ||
102 | // XXX: I'm not sure that frame types 3-5 are invalid | 102 | // XXX: I'm not sure that frame types 3-5 are invalid |
103 | ok(frameType === 1 || frameType === 2, | 103 | ok(frameType === 1 || frameType === 2, |
104 | 'the frame type should be valid'); | 104 | 'the frame type should be valid'); |
105 | 105 | ||
106 | equal(7, codecId, 'the codec ID is AVC for h264'); | 106 | equal(7, codecId, 'the codec ID is AVC for h264'); |
107 | ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]'); | 107 | ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]'); |
108 | if (packetType !== 1) { | 108 | if (packetType !== 1) { |
109 | equal(0, | 109 | equal(0, |
110 | compositionTime, | 110 | compositionTime, |
111 | 'the composition time is zero for non-NALU packets'); | 111 | 'the composition time is zero for non-NALU packets'); |
112 | } | 112 | } |
113 | 113 | ||
114 | // TODO: the rest of the bytes are an NLU unit | 114 | // TODO: the rest of the bytes are an NLU unit |
115 | if (packetType === 0) { | 115 | if (packetType === 0) { |
116 | // AVC decoder configuration record | 116 | // AVC decoder configuration record |
117 | } else { | 117 | } else { |
118 | // NAL units | 118 | // NAL units |
119 | testNalUnit(tag.bytes.subarray(16)); | 119 | testNalUnit(tag.bytes.subarray(16)); |
120 | } | 120 | } |
121 | }; | 121 | }; |
122 | 122 | ||
123 | testNalUnit = function(bytes) { | 123 | testNalUnit = function (bytes) { |
124 | var | 124 | var |
125 | nalHeader = bytes[0], | 125 | nalHeader = bytes[0], |
126 | unitType = nalHeader & 0x1F; | 126 | unitType = nalHeader & 0x1F; |
127 | 127 | ||
128 | equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0'); | 128 | equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0'); |
129 | // equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something'); | 129 | // equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something'); |
130 | // ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0'); | 130 | // ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0'); |
131 | // ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22'); | 131 | // ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22'); |
132 | }; | 132 | }; |
133 | 133 | ||
134 | 134 | ||
135 | asciiFromBytes = function(bytes) { | 135 | asciiFromBytes = function (bytes) { |
136 | var | 136 | var |
137 | string = [], | 137 | string = [], |
138 | i = bytes.byteLength; | 138 | i = bytes.byteLength; |
139 | 139 | ||
140 | while (i--) { | 140 | while (i--) { |
141 | string[i] = String.fromCharCode(bytes[i]); | 141 | string[i] = String.fromCharCode(bytes[i]); |
142 | } | 142 | } |
143 | return string.join(''); | 143 | return string.join(''); |
144 | }; | 144 | }; |
145 | 145 | ||
146 | testScriptString = function(tag, offset, expected) { | 146 | testScriptString = function (tag, offset, expected) { |
147 | var type = tag.bytes[offset], | 147 | var type = tag.bytes[offset], |
148 | stringLength = tag.view.getUint16(offset + 1), | 148 | stringLength = tag.view.getUint16(offset + 1), |
149 | string, | 149 | string, |
150 | i = expected.length; | 150 | i = expected.length; |
151 | 151 | ||
152 | equal(2, type, 'the script element is of string type'); | 152 | equal(2, type, 'the script element is of string type'); |
153 | equal(stringLength, expected.length, 'the script string length is correct'); | 153 | equal(stringLength, expected.length, 'the script string length is correct'); |
154 | string = asciiFromBytes(tag.bytes.subarray(offset + 3, | 154 | string = asciiFromBytes(tag.bytes.subarray(offset + 3, |
155 | offset + 3 + stringLength)); | 155 | offset + 3 + stringLength)); |
156 | equal(expected, string, 'the string value is "' + expected + '"'); | 156 | equal(expected, string, 'the string value is "' + expected + '"'); |
157 | }; | 157 | }; |
158 | 158 | ||
159 | testScriptEcmaArray = function(tag, start) { | 159 | testScriptEcmaArray = function (tag, start) { |
160 | var | 160 | var |
161 | numItems = tag.view.getUint32(start), | 161 | numItems = tag.view.getUint32(start), |
162 | i = numItems, | 162 | i = numItems, |
163 | offset = start + 4, | 163 | offset = start + 4, |
164 | length, | 164 | length, |
165 | type; | 165 | type; |
166 | 166 | ||
167 | while (i--) { | 167 | while (i--) { |
168 | length = tag.view.getUint16(offset); | 168 | length = tag.view.getUint16(offset); |
169 | 169 | ||
170 | // advance offset to the property value | 170 | // advance offset to the property value |
171 | offset += 2 + length; | 171 | offset += 2 + length; |
172 | 172 | ||
173 | type = tag.bytes[offset]; | 173 | type = tag.bytes[offset]; |
174 | ok(type === 1 || type === 0, | 174 | ok(type === 1 || type === 0, |
175 | 'the ecma array property value type is number or boolean'); | 175 | 'the ecma array property value type is number or boolean'); |
176 | offset++; | 176 | offset++; |
177 | if (type) { | 177 | if (type) { |
178 | // boolean | 178 | // boolean |
179 | ok(tag.bytes[offset] === 0 || tag.bytes[offset] === 1, | 179 | ok(tag.bytes[offset] === 0 || tag.bytes[offset] === 1, |
180 | 'the script boolean value is 0 or 1'); | 180 | 'the script boolean value is 0 or 1'); |
181 | offset++; | 181 | offset++; |
182 | } else { | 182 | } else { |
183 | // number | 183 | // number |
184 | ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN'); | 184 | ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN'); |
185 | offset += 8; | 185 | offset += 8; |
186 | } | 186 | } |
187 | } | 187 | } |
188 | equal(tag.bytes[offset], 0, 'the property array terminator is valid'); | 188 | equal(tag.bytes[offset], 0, 'the property array terminator is valid'); |
189 | equal(tag.bytes[offset + 1], 0, 'the property array terminator is valid'); | 189 | equal(tag.bytes[offset + 1], 0, 'the property array terminator is valid'); |
190 | equal(tag.bytes[offset + 2], 9, 'the property array terminator is valid'); | 190 | equal(tag.bytes[offset + 2], 9, 'the property array terminator is valid'); |
191 | }; | 191 | }; |
192 | 192 | ||
193 | testScriptTag = function(tag) { | 193 | testScriptTag = function (tag) { |
194 | testScriptString(tag, 11, 'onMetaData'); | 194 | testScriptString(tag, 11, 'onMetaData'); |
195 | 195 | ||
196 | // the onMetaData object is stored as an 'ecma array', an array with non- | 196 | // the onMetaData object is stored as an 'ecma array', an array with non- |
197 | // integer indices (i.e. a dictionary or hash-map). | 197 | // integer indices (i.e. a dictionary or hash-map). |
198 | equal(8, tag.bytes[24], 'onMetaData is of ecma array type'); | 198 | equal(8, tag.bytes[24], 'onMetaData is of ecma array type'); |
199 | testScriptEcmaArray(tag, 25); | 199 | testScriptEcmaArray(tag, 25); |
200 | }; | 200 | }; |
201 | 201 | ||
202 | test('the flv tags are well-formed', function() { | 202 | test('the flv tags are well-formed', function () { |
203 | var | 203 | var |
204 | tag, | 204 | tag, |
205 | byte, | 205 | byte, |
206 | type, | 206 | type, |
207 | lastTime = 0; | 207 | lastTime = 0; |
208 | parser.parseSegmentBinaryData(window.bcSegment); | 208 | parser.parseSegmentBinaryData(window.bcSegment); |
209 | 209 | ||
210 | while (parser.tagsAvailable()) { | 210 | while (parser.tagsAvailable()) { |
211 | tag = parser.getNextTag(); | 211 | tag = parser.getNextTag(); |
212 | type = tag.bytes[0]; | 212 | type = tag.bytes[0]; |
213 | 213 | ||
214 | // generic flv headers | 214 | // generic flv headers |
215 | ok(type === 8 || type === 9 || type === 18, | 215 | ok(type === 8 || type === 9 || type === 18, |
216 | 'the type field specifies audio, video or script'); | 216 | 'the type field specifies audio, video or script'); |
217 | 217 | ||
218 | byte = (tag.view.getUint32(1) & 0xFFFFFF00) >>> 8; | 218 | byte = (tag.view.getUint32(1) & 0xFFFFFF00) >>> 8; |
219 | equal(tag.bytes.byteLength - 11 - 4, byte, 'the size field is correct'); | 219 | equal(tag.bytes.byteLength - 11 - 4, byte, 'the size field is correct'); |
220 | 220 | ||
221 | byte = tag.view.getUint32(5) & 0xFFFFFF00; | 221 | byte = tag.view.getUint32(5) & 0xFFFFFF00; |
222 | ok(byte >= lastTime, 'the timestamp for the tag is greater than zero'); | 222 | ok(byte >= lastTime, 'the timestamp for the tag is greater than zero'); |
223 | lastTime = byte; | 223 | lastTime = byte; |
224 | 224 | ||
225 | // tag type-specific headers | 225 | // tag type-specific headers |
226 | ({ | 226 | ({ |
227 | 8: testAudioTag, | 227 | 8: testAudioTag, |
228 | 9: testVideoTag, | 228 | 9: testVideoTag, |
229 | 18: testScriptTag | 229 | 18: testScriptTag |
230 | })[type](tag); | 230 | })[type](tag); |
231 | 231 | ||
232 | // previous tag size | 232 | // previous tag size |
233 | equal(tag.bytes.byteLength - 4, | 233 | equal(tag.bytes.byteLength - 4, |
234 | tag.view.getUint32(tag.bytes.byteLength - 4), | 234 | tag.view.getUint32(tag.bytes.byteLength - 4), |
235 | 'the size of the previous tag is correct'); | 235 | 'the size of the previous tag is correct'); |
236 | } | 236 | } |
237 | }); | 237 | }); |
238 | 238 | ||
239 | /* | 239 | /* |
240 | M3U8 Test Suite | 240 | M3U8 Test Suite |
241 | */ | 241 | */ |
242 | 242 | ||
243 | module('m3u8 parser', { | 243 | module('m3u8 parser', { |
244 | setup: function() { | 244 | setup: function () { |
245 | m3u8parser = new window.videojs.hls.M3U8Parser(); | 245 | m3u8parser = new window.videojs.hls.M3U8Parser(); |
246 | } | 246 | } |
247 | }); | 247 | }); |
248 | 248 | ||
249 | test('should create my parser', function() { | 249 | test('should create my parser', function () { |
250 | ok(m3u8parser != undefined); | 250 | ok(m3u8parser != undefined); |
251 | } | 251 | } |
252 | ); | 252 | ); |
253 | 253 | ||
254 | test('should successfully parse manifest data', function() { | 254 | test('should successfully parse manifest data', function () { |
255 | var parsedData = m3u8parser.parse(window.playlistData); | 255 | var parsedData = m3u8parser.parse(window.playlistData); |
256 | ok(parsedData); | 256 | ok(parsedData); |
257 | } | 257 | } |
258 | ); | 258 | ); |
259 | 259 | ||
260 | test('test for expected results', function() { | 260 | test('test for expected results', function () { |
261 | var data = m3u8parser.parse(window.playlistData); | 261 | var data = m3u8parser.parse(window.playlistData); |
262 | 262 | ||
263 | notEqual(data, null, 'data is not NULL'); | 263 | notEqual(data, null, 'data is not NULL'); |
264 | equal(data.invalidReasons.length, 0,'data has 0 invalid reasons'); | 264 | equal(data.invalidReasons.length, 0, 'data has 0 invalid reasons'); |
265 | equal(data.hasValidM3UTag, true, 'data has valid EXTM3U'); | 265 | equal(data.hasValidM3UTag, true, 'data has valid EXTM3U'); |
266 | equal(data.targetDuration, 10, 'data has correct TARGET DURATION'); | 266 | equal(data.targetDuration, 10, 'data has correct TARGET DURATION'); |
267 | equal(data.allowCache, "NO", 'acceptable ALLOW CACHE'); | 267 | equal(data.allowCache, "NO", 'acceptable ALLOW CACHE'); |
... | @@ -275,109 +275,109 @@ | ... | @@ -275,109 +275,109 @@ |
275 | ); | 275 | ); |
276 | 276 | ||
277 | module('brightcove playlist', { | 277 | module('brightcove playlist', { |
278 | setup: function() { | 278 | setup: function () { |
279 | m3u8parser = new window.videojs.hls.M3U8Parser(); | 279 | m3u8parser = new window.videojs.hls.M3U8Parser(); |
280 | } | 280 | } |
281 | }); | 281 | }); |
282 | 282 | ||
283 | test('should parse a brightcove manifest data', function() { | 283 | test('should parse a brightcove manifest data', function () { |
284 | var data = m3u8parser.parse(window.brightcove_playlist_data); | 284 | var data = m3u8parser.parse(window.brightcove_playlist_data); |
285 | 285 | ||
286 | ok(data); | 286 | ok(data); |
287 | equal(data.playlistItems.length, 4, 'Has correct rendition count'); | 287 | equal(data.playlistItems.length, 4, 'Has correct rendition count'); |
288 | equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct' ); | 288 | equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct'); |
289 | equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct' ); | 289 | equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct'); |
290 | equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct' ); | 290 | equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct'); |
291 | equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct' ); | 291 | equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct'); |
292 | 292 | ||
293 | } | 293 | } |
294 | ); | 294 | ); |
295 | 295 | ||
296 | module('manifest controller', { | 296 | module('manifest controller', { |
297 | setup: function() { | 297 | setup: function () { |
298 | manifestController = new window.videojs.hls.ManifestController(); | 298 | manifestController = new window.videojs.hls.ManifestController(); |
299 | this.vjsget = vjs.get; | 299 | this.vjsget = vjs.get; |
300 | vjs.get = function(url, success, error){ | 300 | vjs.get = function (url, success, error) { |
301 | console.log(url); | 301 | console.log(url); |
302 | success(window.brightcove_playlist_data); | 302 | success(window.brightcove_playlist_data); |
303 | }; | 303 | }; |
304 | }, | 304 | }, |
305 | teardown: function() { | 305 | teardown: function () { |
306 | vjs.get = this.vjsget; | 306 | vjs.get = this.vjsget; |
307 | } | 307 | } |
308 | }); | 308 | }); |
309 | 309 | ||
310 | test('should create', function() { | 310 | test('should create', function () { |
311 | ok(manifestController); | 311 | ok(manifestController); |
312 | }); | 312 | }); |
313 | 313 | ||
314 | test('should return a parsed object', function() { | 314 | test('should return a parsed object', function () { |
315 | var data = manifestController.parseManifest(window.brightcove_playlist_data); | 315 | var data = manifestController.parseManifest(window.brightcove_playlist_data); |
316 | 316 | ||
317 | ok(data); | 317 | ok(data); |
318 | 318 | ||
319 | equal(data.playlistItems.length, 4, 'Has correct rendition count'); | 319 | equal(data.playlistItems.length, 4, 'Has correct rendition count'); |
320 | equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct' ); | 320 | equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct'); |
321 | equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct' ); | 321 | equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct'); |
322 | equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct' ); | 322 | equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct'); |
323 | equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct' ); | 323 | equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct'); |
324 | }) | 324 | }) |
325 | 325 | ||
326 | test('should get a manifest from hermes', function() { | 326 | test('should get a manifest from hermes', function () { |
327 | var hermesUrl = "http://localhost:7070/test/basic-playback/brightcove/16x9-master.m3u8"; | 327 | var hermesUrl = "http://localhost:7070/test/basic-playback/brightcove/16x9-master.m3u8"; |
328 | 328 | ||
329 | manifestController.loadManifest( | 329 | manifestController.loadManifest( |
330 | hermesUrl, | 330 | hermesUrl, |
331 | function(responseData){ | 331 | function (responseData) { |
332 | ok(true); | 332 | ok(true); |
333 | }, | 333 | }, |
334 | function(errorData){ | 334 | function (errorData) { |
335 | console.log('got error data'); | 335 | console.log('got error data'); |
336 | }, | 336 | }, |
337 | function(updateData){ | 337 | function (updateData) { |
338 | console.log('got update data'); | 338 | console.log('got update data'); |
339 | } | 339 | } |
340 | ) | 340 | ) |
341 | }); | 341 | }); |
342 | 342 | ||
343 | module('segment controller', { | 343 | module('segment controller', { |
344 | setup: function() { | 344 | setup: function () { |
345 | segmentController = new window.videojs.hls.SegmentController(); | 345 | segmentController = new window.videojs.hls.SegmentController(); |
346 | this.vjsget = vjs.get; | 346 | this.vjsget = vjs.get; |
347 | vjs.get = function(url, success, error){ | 347 | vjs.get = function (url, success, error) { |
348 | console.log('load segment url', url); | 348 | console.log('load segment url', url); |
349 | success(window.bcSegment); | 349 | success(window.bcSegment); |
350 | }; | 350 | }; |
351 | }, | 351 | }, |
352 | teardown: function() { | 352 | teardown: function () { |
353 | vjs.get = this.vjsget; | 353 | vjs.get = this.vjsget; |
354 | } | 354 | } |
355 | }); | 355 | }); |
356 | 356 | ||
357 | test('should get a segment data', function() { | 357 | test('should get a segment data', function () { |
358 | ok(true); | 358 | ok(true); |
359 | var hermesUrl = "http://localhost:7070/test/ts-files/brightcove/s-1.ts"; | 359 | var hermesUrl = "http://localhost:7070/test/ts-files/brightcove/s-1.ts"; |
360 | 360 | ||
361 | segmentController.loadSegment( | 361 | segmentController.loadSegment( |
362 | hermesUrl, | 362 | hermesUrl, |
363 | function(responseData){ | 363 | function (responseData) { |
364 | console.log('got response from segment controller'); | 364 | console.log('got response from segment controller'); |
365 | ok(true); | 365 | ok(true); |
366 | 366 | ||
367 | }, | 367 | }, |
368 | function(errorData){ | 368 | function (errorData) { |
369 | console.log('got error data'); | 369 | console.log('got error data'); |
370 | }, | 370 | }, |
371 | function(updateData){ | 371 | function (updateData) { |
372 | console.log('got update data'); | 372 | console.log('got update data'); |
373 | } | 373 | } |
374 | ) | 374 | ) |
375 | } | 375 | } |
376 | ) | 376 | ) |
377 | 377 | ||
378 | test('bandwidth calulation test', function() { | 378 | test('bandwidth calulation test', function () { |
379 | var multiSecondData = segmentController.calculateThroughput(10000,1000,2000); | 379 | var multiSecondData = segmentController.calculateThroughput(10000, 1000, 2000); |
380 | var subSecondData = segmentController.calculateThroughput(10000,1000,1500); | 380 | var subSecondData = segmentController.calculateThroughput(10000, 1000, 1500); |
381 | equal(multiSecondData, 80000, 'MULTI-Second bits per second calculation'); | 381 | equal(multiSecondData, 80000, 'MULTI-Second bits per second calculation'); |
382 | equal(subSecondData, 160000, 'SUB-Second bits per second calculation'); | 382 | equal(subSecondData, 160000, 'SUB-Second bits per second calculation'); |
383 | 383 | ... | ... |
-
Please register or sign in to post a comment