73540062 by Brandon Casey Committed by GitHub

got the netowrk-switcher working (#766)

1 parent 1837d8d9
...@@ -40,9 +40,10 @@ ...@@ -40,9 +40,10 @@
40 "version": "npm run build", 40 "version": "npm run build",
41 "watch": "npm-run-all -p watch:*", 41 "watch": "npm-run-all -p watch:*",
42 "watch:docs": "nodemon --watch src/ --exec npm run docs", 42 "watch:docs": "nodemon --watch src/ --exec npm run docs",
43 "watch:js": "npm-run-all -p watch:js:babel watch:js:browserify", 43 "watch:js": "npm-run-all -p watch:js:babel watch:js:browserify watch:js:switcher",
44 "watch:js:babel": "npm run build:js:babel -- --watch", 44 "watch:js:babel": "npm run build:js:babel -- --watch",
45 "watch:js:browserify": "watchify . -v -g browserify-shim -o dist/videojs-contrib-hls.js", 45 "watch:js:browserify": "watchify . -v -g browserify-shim -o dist/videojs-contrib-hls.js",
46 "watch:js:switcher": "watchify utils/switcher/switcher.js -v -t babelify -g browserify-shim -o dist/switcher.js",
46 "watch:test": "npm-run-all -p watch:test:*", 47 "watch:test": "npm-run-all -p watch:test:*",
47 "watch:test:js": "node scripts/watch-test.js", 48 "watch:test:js": "node scripts/watch-test.js",
48 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", 49 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
...@@ -98,6 +99,7 @@ ...@@ -98,6 +99,7 @@
98 "browserify-shim": "^3.0.0", 99 "browserify-shim": "^3.0.0",
99 "connect": "^3.4.0", 100 "connect": "^3.4.0",
100 "cowsay": "^1.1.0", 101 "cowsay": "^1.1.0",
102 "d3": "3.4.8",
101 "doctoc": "^0.15.0", 103 "doctoc": "^0.15.0",
102 "glob": "^6.0.3", 104 "glob": "^6.0.3",
103 "jsdoc": "^3.4.0", 105 "jsdoc": "^3.4.0",
......
...@@ -117,7 +117,9 @@ let fakeEnvironment = { ...@@ -117,7 +117,9 @@ let fakeEnvironment = {
117 this.xhr.restore(); 117 this.xhr.restore();
118 ['warn', 'error'].forEach((level) => { 118 ['warn', 'error'].forEach((level) => {
119 if (this.log && this.log[level] && this.log[level].restore) { 119 if (this.log && this.log[level] && this.log[level].restore) {
120 QUnit.equal(this.log[level].callCount, 0, `no unexpected logs on ${level}`); 120 if (QUnit) {
121 QUnit.equal(this.log[level].callCount, 0, `no unexpected logs on ${level}`);
122 }
121 this.log[level].restore(); 123 this.log[level].restore();
122 } 124 }
123 }); 125 });
...@@ -256,7 +258,7 @@ export const openMediaSource = function(player, clock) { ...@@ -256,7 +258,7 @@ export const openMediaSource = function(player, clock) {
256 }); 258 });
257 }; 259 };
258 260
259 export const standardXHRResponse = function(request) { 261 export const standardXHRResponse = function(request, data) {
260 if (!request.url) { 262 if (!request.url) {
261 return; 263 return;
262 } 264 }
...@@ -277,9 +279,12 @@ export const standardXHRResponse = function(request) { ...@@ -277,9 +279,12 @@ export const standardXHRResponse = function(request) {
277 contentType = 'video/MP2T'; 279 contentType = 'video/MP2T';
278 } 280 }
279 281
282 if (!data) {
283 data = testDataManifests[manifestName];
284 }
285
280 request.response = new Uint8Array(16).buffer; 286 request.response = new Uint8Array(16).buffer;
281 request.respond(200, { 'Content-Type': contentType }, 287 request.respond(200, {'Content-Type': contentType}, data);
282 testDataManifests[manifestName]);
283 }; 288 };
284 289
285 // return an absolute version of a page-relative URL 290 // return an absolute version of a page-relative URL
......
1 /*! normalize.css v1.1.2 | MIT License | git.io/normalize */
2
3 /* ==========================================================================
4 HTML5 display definitions
5 ========================================================================== */
6
7 /**
8 * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3.
9 */
10
11 article,
12 aside,
13 details,
14 figcaption,
15 figure,
16 footer,
17 header,
18 hgroup,
19 main,
20 nav,
21 section,
22 summary {
23 display: block;
24 }
25
26 /**
27 * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
28 */
29
30 audio,
31 canvas,
32 video {
33 display: inline-block;
34 *display: inline;
35 *zoom: 1;
36 }
37
38 /**
39 * Prevent modern browsers from displaying `audio` without controls.
40 * Remove excess height in iOS 5 devices.
41 */
42
43 audio:not([controls]) {
44 display: none;
45 height: 0;
46 }
47
48 /**
49 * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
50 * Known issue: no IE 6 support.
51 */
52
53 [hidden] {
54 display: none;
55 }
56
57 /* ==========================================================================
58 Base
59 ========================================================================== */
60
61 /**
62 * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
63 * `em` units.
64 * 2. Prevent iOS text size adjust after orientation change, without disabling
65 * user zoom.
66 */
67
68 html {
69 font-size: 100%; /* 1 */
70 -ms-text-size-adjust: 100%; /* 2 */
71 -webkit-text-size-adjust: 100%; /* 2 */
72 }
73
74 /**
75 * Address `font-family` inconsistency between `textarea` and other form
76 * elements.
77 */
78
79 html,
80 button,
81 input,
82 select,
83 textarea {
84 font-family: sans-serif;
85 }
86
87 /**
88 * Address margins handled incorrectly in IE 6/7.
89 */
90
91 body {
92 margin: 0;
93 }
94
95 /* ==========================================================================
96 Links
97 ========================================================================== */
98
99 /**
100 * Address `outline` inconsistency between Chrome and other browsers.
101 */
102
103 a:focus {
104 outline: thin dotted;
105 }
106
107 /**
108 * Improve readability when focused and also mouse hovered in all browsers.
109 */
110
111 a:active,
112 a:hover {
113 outline: 0;
114 }
115
116 /* ==========================================================================
117 Typography
118 ========================================================================== */
119
120 /**
121 * Address font sizes and margins set differently in IE 6/7.
122 * Address font sizes within `section` and `article` in Firefox 4+, Safari 5,
123 * and Chrome.
124 */
125
126 h1 {
127 font-size: 2em;
128 margin: 0.67em 0;
129 }
130
131 h2 {
132 font-size: 1.5em;
133 margin: 0.83em 0;
134 }
135
136 h3 {
137 font-size: 1.17em;
138 margin: 1em 0;
139 }
140
141 h4 {
142 font-size: 1em;
143 margin: 1.33em 0;
144 }
145
146 h5 {
147 font-size: 0.83em;
148 margin: 1.67em 0;
149 }
150
151 h6 {
152 font-size: 0.67em;
153 margin: 2.33em 0;
154 }
155
156 /**
157 * Address styling not present in IE 7/8/9, Safari 5, and Chrome.
158 */
159
160 abbr[title] {
161 border-bottom: 1px dotted;
162 }
163
164 /**
165 * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome.
166 */
167
168 b,
169 strong {
170 font-weight: bold;
171 }
172
173 blockquote {
174 margin: 1em 40px;
175 }
176
177 /**
178 * Address styling not present in Safari 5 and Chrome.
179 */
180
181 dfn {
182 font-style: italic;
183 }
184
185 /**
186 * Address differences between Firefox and other browsers.
187 * Known issue: no IE 6/7 normalization.
188 */
189
190 hr {
191 -moz-box-sizing: content-box;
192 box-sizing: content-box;
193 height: 0;
194 }
195
196 /**
197 * Address styling not present in IE 6/7/8/9.
198 */
199
200 mark {
201 background: #ff0;
202 color: #000;
203 }
204
205 /**
206 * Address margins set differently in IE 6/7.
207 */
208
209 p,
210 pre {
211 margin: 1em 0;
212 }
213
214 /**
215 * Correct font family set oddly in IE 6, Safari 4/5, and Chrome.
216 */
217
218 code,
219 kbd,
220 pre,
221 samp {
222 font-family: monospace, serif;
223 _font-family: 'courier new', monospace;
224 font-size: 1em;
225 }
226
227 /**
228 * Improve readability of pre-formatted text in all browsers.
229 */
230
231 pre {
232 white-space: pre;
233 white-space: pre-wrap;
234 word-wrap: break-word;
235 }
236
237 /**
238 * Address CSS quotes not supported in IE 6/7.
239 */
240
241 q {
242 quotes: none;
243 }
244
245 /**
246 * Address `quotes` property not supported in Safari 4.
247 */
248
249 q:before,
250 q:after {
251 content: '';
252 content: none;
253 }
254
255 /**
256 * Address inconsistent and variable font size in all browsers.
257 */
258
259 small {
260 font-size: 80%;
261 }
262
263 /**
264 * Prevent `sub` and `sup` affecting `line-height` in all browsers.
265 */
266
267 sub,
268 sup {
269 font-size: 75%;
270 line-height: 0;
271 position: relative;
272 vertical-align: baseline;
273 }
274
275 sup {
276 top: -0.5em;
277 }
278
279 sub {
280 bottom: -0.25em;
281 }
282
283 /* ==========================================================================
284 Lists
285 ========================================================================== */
286
287 /**
288 * Address margins set differently in IE 6/7.
289 */
290
291 dl,
292 menu,
293 ol,
294 ul {
295 margin: 1em 0;
296 }
297
298 dd {
299 margin: 0 0 0 40px;
300 }
301
302 /**
303 * Address paddings set differently in IE 6/7.
304 */
305
306 menu,
307 ol,
308 ul {
309 padding: 0 0 0 40px;
310 }
311
312 /**
313 * Correct list images handled incorrectly in IE 7.
314 */
315
316 nav ul,
317 nav ol {
318 list-style: none;
319 list-style-image: none;
320 }
321
322 /* ==========================================================================
323 Embedded content
324 ========================================================================== */
325
326 /**
327 * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
328 * 2. Improve image quality when scaled in IE 7.
329 */
330
331 img {
332 border: 0; /* 1 */
333 -ms-interpolation-mode: bicubic; /* 2 */
334 }
335
336 /**
337 * Correct overflow displayed oddly in IE 9.
338 */
339
340 svg:not(:root) {
341 overflow: hidden;
342 }
343
344 /* ==========================================================================
345 Figures
346 ========================================================================== */
347
348 /**
349 * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
350 */
351
352 figure {
353 margin: 0;
354 }
355
356 /* ==========================================================================
357 Forms
358 ========================================================================== */
359
360 /**
361 * Correct margin displayed oddly in IE 6/7.
362 */
363
364 form {
365 margin: 0;
366 }
367
368 /**
369 * Define consistent border, margin, and padding.
370 */
371
372 fieldset {
373 border: 1px solid #c0c0c0;
374 margin: 0 2px;
375 padding: 0.35em 0.625em 0.75em;
376 }
377
378 /**
379 * 1. Correct color not being inherited in IE 6/7/8/9.
380 * 2. Correct text not wrapping in Firefox 3.
381 * 3. Correct alignment displayed oddly in IE 6/7.
382 */
383
384 legend {
385 border: 0; /* 1 */
386 padding: 0;
387 white-space: normal; /* 2 */
388 *margin-left: -7px; /* 3 */
389 }
390
391 /**
392 * 1. Correct font size not being inherited in all browsers.
393 * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
394 * and Chrome.
395 * 3. Improve appearance and consistency in all browsers.
396 */
397
398 button,
399 input,
400 select,
401 textarea {
402 font-size: 100%; /* 1 */
403 margin: 0; /* 2 */
404 vertical-align: baseline; /* 3 */
405 *vertical-align: middle; /* 3 */
406 }
407
408 /**
409 * Address Firefox 3+ setting `line-height` on `input` using `!important` in
410 * the UA stylesheet.
411 */
412
413 button,
414 input {
415 line-height: normal;
416 }
417
418 /**
419 * Address inconsistent `text-transform` inheritance for `button` and `select`.
420 * All other form control elements do not inherit `text-transform` values.
421 * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
422 * Correct `select` style inheritance in Firefox 4+ and Opera.
423 */
424
425 button,
426 select {
427 text-transform: none;
428 }
429
430 /**
431 * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
432 * and `video` controls.
433 * 2. Correct inability to style clickable `input` types in iOS.
434 * 3. Improve usability and consistency of cursor style between image-type
435 * `input` and others.
436 * 4. Remove inner spacing in IE 7 without affecting normal text inputs.
437 * Known issue: inner spacing remains in IE 6.
438 */
439
440 button,
441 html input[type="button"], /* 1 */
442 input[type="reset"],
443 input[type="submit"] {
444 -webkit-appearance: button; /* 2 */
445 cursor: pointer; /* 3 */
446 *overflow: visible; /* 4 */
447 }
448
449 /**
450 * Re-set default cursor for disabled elements.
451 */
452
453 button[disabled],
454 html input[disabled] {
455 cursor: default;
456 }
457
458 /**
459 * 1. Address box sizing set to content-box in IE 8/9.
460 * 2. Remove excess padding in IE 8/9.
461 * 3. Remove excess padding in IE 7.
462 * Known issue: excess padding remains in IE 6.
463 */
464
465 input[type="checkbox"],
466 input[type="radio"] {
467 box-sizing: border-box; /* 1 */
468 padding: 0; /* 2 */
469 *height: 13px; /* 3 */
470 *width: 13px; /* 3 */
471 }
472
473 /**
474 * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
475 * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
476 * (include `-moz` to future-proof).
477 */
478
479 input[type="search"] {
480 -webkit-appearance: textfield; /* 1 */
481 -moz-box-sizing: content-box;
482 -webkit-box-sizing: content-box; /* 2 */
483 box-sizing: content-box;
484 }
485
486 /**
487 * Remove inner padding and search cancel button in Safari 5 and Chrome
488 * on OS X.
489 */
490
491 input[type="search"]::-webkit-search-cancel-button,
492 input[type="search"]::-webkit-search-decoration {
493 -webkit-appearance: none;
494 }
495
496 /**
497 * Remove inner padding and border in Firefox 3+.
498 */
499
500 button::-moz-focus-inner,
501 input::-moz-focus-inner {
502 border: 0;
503 padding: 0;
504 }
505
506 /**
507 * 1. Remove default vertical scrollbar in IE 6/7/8/9.
508 * 2. Improve readability and alignment in all browsers.
509 */
510
511 textarea {
512 overflow: auto; /* 1 */
513 vertical-align: top; /* 2 */
514 }
515
516 /* ==========================================================================
517 Tables
518 ========================================================================== */
519
520 /**
521 * Remove most spacing between table cells.
522 */
523
524 table {
525 border-collapse: collapse;
526 border-spacing: 0;
527 }
1 /*! normalize.css v1.1.2 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}
...\ No newline at end of file ...\ No newline at end of file
1 // render the timeline with d3
2 let timeline = document.querySelector('.timeline');
3 timeline.innerHTML = '';
4
5 let margin = {
6 top: 20,
7 right: 80,
8 bottom: 30,
9 left: 50
10 };
11 let width = 960 - margin.left - margin.right;
12 let height = 500 - margin.top - margin.bottom;
13 let svg = d3.select('.timeline').append('svg')
14 .attr('width', width + margin.left + margin.right)
15 .attr('height', height + margin.top + margin.bottom)
16 .append('g')
17 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
18
19
20 const displayTimeline = function(error, data) {
21 var x = d3.scale.linear().range([0, width]),
22 y = d3.scale.linear().range([height, 0]),
23
24 timeAxis = d3.svg.axis().scale(x).orient('bottom'),
25 tickFormatter = d3.format(',.0f'),
26 bitrateAxis = d3.svg.axis()
27 .scale(y)
28 .tickFormat(function(value) {
29 return tickFormatter(value / 1024);
30 })
31 .orient('left'),
32
33 bandwidthLine = d3.svg.line()
34 .interpolate('basis')
35 .x(function(data) {
36 return x(data.time);
37 })
38 .y(function(data) {
39 return y(data.bandwidth);
40 }),
41 effectiveBandwidthLine = d3.svg.line()
42 .interpolate('basis')
43 .x(function(data) {
44 return x(data.time);
45 })
46 .y(function(data) {
47 return y(data.bandwidth);
48 });
49
50 x.domain(d3.extent(data.bandwidth, function(data) {
51 return data.time;
52 }));
53 y.domain([0, Math.max(d3.max(data.bandwidth, function(data) {
54 return data.bandwidth;
55 }), d3.max(data.options.playlists), d3.max(data.playlists, function(data) {
56 return data.bitrate;
57 }))]);
58
59 // time axis
60 svg.selectAll('.axis').remove();
61 svg.append('g')
62 .attr('class', 'x axis')
63 .attr('transform', 'translate(0,' + height + ')')
64 .call(timeAxis);
65
66 // bitrate axis
67 svg.append('g')
68 .attr('class', 'y axis')
69 .call(bitrateAxis)
70 .append('text')
71 .attr('transform', 'rotate(-90)')
72 .attr('y', 6)
73 .attr('dy', '.71em')
74 .style('text-anchor', 'end')
75 .text('Bitrate (kb/s)');
76
77 // playlist bitrate lines
78 svg.selectAll('.line.bitrate').remove();
79 svg.selectAll('.line.bitrate')
80 .data(data.options.playlists)
81 .enter().append('path')
82 .attr('class', 'line bitrate')
83 .attr('d', function(playlist) {
84 return 'M0,' + y(playlist) + 'L' + width + ',' + y(playlist);
85 });
86
87 // bandwidth line
88 svg.selectAll('.bandwidth').remove();
89 svg.append('path')
90 .datum(data.bandwidth)
91 .attr('class', 'line bandwidth')
92 .attr('d', bandwidthLine);
93 svg.selectAll('.effective-bandwidth').remove();
94 svg.append('path')
95 .datum(data.effectiveBandwidth)
96 .attr('class', 'line effective-bandwidth')
97 .attr('d', effectiveBandwidthLine);
98
99 svg.append('text')
100 .attr('class', 'bandwidth label')
101 .attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.bandwidth.slice(-1)[0].bandwidth) + ')')
102 .attr('dy', '1.35em')
103 .text('bandwidth');
104 svg.append('text')
105 .attr('class', 'bandwidth label')
106 .attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.effectiveBandwidth.slice(-1)[0].bandwidth) + ')')
107 .attr('dy', '1.35em')
108 .text('measured');
109
110 // segment bitrate dots
111 svg.selectAll('.segment-bitrate').remove();
112 svg.selectAll('.segment-bitrate')
113 .data(data.playlists)
114 .enter().append('circle')
115 .attr('class', 'dot segment-bitrate')
116 .attr('r', 3.5)
117 .attr('cx', function(playlist) {
118 return x(playlist.time);
119 })
120 .attr('cy', function(playlist) {
121 return y(playlist.bitrate);
122 });
123
124 // highlight intervals when the buffer is empty
125 svg.selectAll('.buffer-empty').remove();
126 svg.selectAll('.buffer-empty')
127 .data(data.buffered.reduce(function(result, sample) {
128 var last = result[result.length - 1];
129 if (sample.buffered === 0) {
130 if (last && sample.time === last.end + 1) {
131 // add this sample to the interval we're accumulating
132 return result.slice(0, result.length - 1).concat({
133 start: last.start,
134 end: sample.time
135 });
136 } else {
137 // this sample starts a new interval
138 return result.concat({
139 start: sample.time,
140 end: sample.time
141 });
142 }
143 }
144 // filter out time periods where the buffer isn't empty
145 return result;
146 }, []))
147 .enter().append('rect')
148 .attr('class', 'buffer-empty')
149 .attr('x', function(data) {
150 return x(data.start);
151 })
152 .attr('width', function(data) {
153 return x(1 + data.end - data.start);
154 })
155 .attr('y', 0)
156 .attr('height', y(height));
157 };
158
159 export default displayTimeline;
1 <!DOCTYPE html> 1 <html>
2 <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
3 <!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
4 <!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
5 <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
6 <head> 2 <head>
7 <meta charset="utf-8"> 3 <meta charset="utf-8">
8 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
9 <title>Playlist Switching Simulator</title> 4 <title>Playlist Switching Simulator</title>
10 <meta name="description" content=""> 5 <link rel="stylesheet" href="switcher.css">
11 <meta name="viewport" content="width=device-width"> 6 <link rel="stylesheet" href="/node_modules/video.js/dist/video-js.css">
12
13 <link rel="stylesheet" href="css/normalize.min.css">
14 <link rel="stylesheet" href="css/main.css">
15 <link rel="stylesheet" href="../../node_modules/video.js/dist/video-js.css">
16
17 <script src="js/vendor/modernizr-2.6.2.min.js"></script>
18 </head> 7 </head>
19 <body> 8 <body>
20 <!--[if lt IE 7]>
21 <p class="chromeframe">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> to improve your experience.</p>
22 <![endif]-->
23
24 <div class="header-container"> 9 <div class="header-container">
25 <header class="wrapper clearfix"> 10 <header class="wrapper clearfix">
26 <h1 class="title">Playlist Switching Simulator</h1> 11 <h1 class="title">Playlist Switching Simulator</h1>
...@@ -29,7 +14,6 @@ ...@@ -29,7 +14,6 @@
29 14
30 <div class="main-container"> 15 <div class="main-container">
31 <div class="main wrapper clearfix"> 16 <div class="main wrapper clearfix">
32
33 <article> 17 <article>
34 <header> 18 <header>
35 <p> 19 <p>
...@@ -38,11 +22,6 @@ ...@@ -38,11 +22,6 @@
38 can helpful to understand how tweaks to the switching 22 can helpful to understand how tweaks to the switching
39 logic will affect playback. 23 logic will affect playback.
40 </p> 24 </p>
41 <p>
42 Flash security restrictions prevent this page from running
43 over the file protocol. Run <code>grunt connect</code> and
44 then <a href="http://localhost:9999/test/switcher">reload.</a>
45 </p>
46 </header> 25 </header>
47 <section> 26 <section>
48 <h2>Timeline</h2> 27 <h2>Timeline</h2>
...@@ -58,7 +37,7 @@ ...@@ -58,7 +37,7 @@
58 After <input name=time0 class=time type=number value=0 min=0> seconds, 37 After <input name=time0 class=time type=number value=0 min=0> seconds,
59 </label> 38 </label>
60 <label> 39 <label>
61 the link capacity is <input name=bandwidth0 class=bandwidth type=number value=921600> bits per second 40 the link capacity is <input name=bandwidth0 class=bandwidth type=number value=250000> bits per second
62 </label> 41 </label>
63 </li> 42 </li>
64 <li> 43 <li>
...@@ -66,7 +45,7 @@ ...@@ -66,7 +45,7 @@
66 After <input name=time1 class=time type=number value=150 min=0> seconds, 45 After <input name=time1 class=time type=number value=150 min=0> seconds,
67 </label> 46 </label>
68 <label> 47 <label>
69 the link capacity is <input name=bandwidth1 class=bandwidth type=number value=450560> bits per second 48 the link capacity is <input name=bandwidth1 class=bandwidth type=number value=500000> bits per second
70 </label> 49 </label>
71 </li> 50 </li>
72 <li> 51 <li>
...@@ -74,7 +53,7 @@ ...@@ -74,7 +53,7 @@
74 After <input name=time2 class=time type=number value=600 min=0> seconds, 53 After <input name=time2 class=time type=number value=600 min=0> seconds,
75 </label> 54 </label>
76 <label> 55 <label>
77 the link capacity is <input name=bandwidth2 class=bandwidth type=number value=1843200> bits per second 56 the link capacity is <input name=bandwidth2 class=bandwidth type=number value=250000> bits per second
78 </label> 57 </label>
79 </li> 58 </li>
80 </ol> 59 </ol>
...@@ -94,44 +73,28 @@ ...@@ -94,44 +73,28 @@
94 </p> 73 </p>
95 The video is available at 74 The video is available at
96 <ul> 75 <ul>
97 <li><input class=bitrate type=number min=1 value=312000> bits per second</li> 76 <li><input class=bitrate type=number min=1 value=52400> bits per second</li>
98 <li><input class=bitrate type=number min=1 value=524000> bits per second</li> 77 <li><input class=bitrate type=number min=1 value=129600> bits per second</li>
99 <li><input class=bitrate type=number min=1 value=1296000> bits per second</li> 78 <li><input class=bitrate type=number min=1 value=212500> bits per second</li>
100 <li><input class=bitrate type=number min=1 value=2125000> bits per second</li> 79 <li><input class=bitrate type=number min=1 value=312500> bits per second</li>
101 <li><input class=bitrate type=number min=1 value=3125000> bits per second</li>
102 </ul> 80 </ul>
103 </form> 81 </form>
104 </section> 82 </section>
105 </article> 83 </article>
106 84
107 </div> <!-- #main --> 85 </div>
108 </div> <!-- #main-container --> 86 </div>
109 87
110 <div class="footer-container"> 88 <div class="footer-container">
111 <footer class="wrapper"> 89 <footer class="wrapper">
112 <h3>videojs-contrib-hls</h3> 90 <h3>videojs-contrib-hls</h3>
113 </footer> 91 </footer>
114 </div> 92 </div>
115 <div id=fixture></div> 93 <div id="qunit-fixture"></div>
116 <script src="../../node_modules/sinon/lib/sinon.js"></script> 94 <script src="/node_modules/sinon/pkg/sinon.js"></script>
117 <script src="../../node_modules/sinon/lib/sinon/util/event.js"></script>
118 <script src="../../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js"></script>
119 <script src="../../node_modules/sinon/lib/sinon/util/xhr_ie.js"></script>
120 <script src="../../node_modules/sinon/lib/sinon/util/fake_timers.js"></script>
121 <script src="js/vendor/d3.min.js"></script>
122
123 <script src="/node_modules/video.js/dist/video.js"></script> 95 <script src="/node_modules/video.js/dist/video.js"></script>
124 <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> 96 <script src="/dist/videojs-contrib-hls.js"></script>
125 <script src="/node_modules/pkcs7/dist/pkcs7.unpad.js"></script> 97 <script src="/node_modules/d3/d3.min.js"></script>
126 <script src="/src/videojs-hls.js"></script> 98 <script src="/dist/switcher.js"></script>
127 <script src="/src/xhr.js"></script>
128 <script src="/src/stream.js"></script>
129 <script src="/src/m3u8/m3u8-parser.js"></script>
130 <script src="/src/playlist.js"></script>
131 <script src="/src/playlist-loader.js"></script>
132 <script src="/src/decrypter.js"></script>
133 <script src="/src/bin-utils.js"></script>
134
135 <script src="js/switcher.js"></script>
136 </body> 99 </body>
137 </html> 100 </html>
......
1 (function(window, document) {
2 'use strict';
3
4 // the number of seconds of video in each segment
5 var segmentDuration = 9, // seconds
6
7 // the number of segments in the video
8 segmentCount = 100,
9
10 // the length of the simulation
11 duration = segmentDuration * segmentCount,
12
13 // the number of seconds it takes for a single bit to be
14 // transmitted from the client to the server, or vice-versa
15 propagationDelay = 0.5,
16
17 runSimulation,
18 playlistResponse,
19 player,
20 runButton,
21 parameters,
22 timeline,
23
24 displayTimeline;
25
26 // mock out the environment and dependencies
27 videojs.options.flash.swf = '../../node_modules/video.js/dist/video-js/video-js.swf';
28 videojs.Hls.SegmentParser = function() {
29 this.getFlvHeader = function() {
30 return new Uint8Array([]);
31 };
32 this.parseSegmentBinaryData = function() {};
33 this.flushTags = function() {};
34 this.tagsAvailable = function() {
35 return false;
36 };
37 this.metadataStream = {
38 on: function() {}
39 };
40 };
41
42 // a dynamic number of time-bandwidth pairs may be defined to drive the simulation
43 (function() {
44 var params,
45 networkTimeline = document.querySelector('.network-timeline'),
46 timePeriod = networkTimeline.querySelector('li:last-child').cloneNode(true),
47 appendTimePeriod = function() {
48 var clone = timePeriod.cloneNode(true),
49 count = networkTimeline.querySelectorAll('input.bandwidth').length,
50 time = clone.querySelector('.time'),
51 bandwidth = clone.querySelector('input.bandwidth');
52
53 time.name = 'time' + count;
54 bandwidth.name = 'bandwidth' + count;
55 networkTimeline.appendChild(clone);
56 };
57 document.querySelector('.add-time-period')
58 .addEventListener('click', appendTimePeriod);
59
60 // apply any simulation parameters that were set in the fragment identifier
61 if (!window.location.hash) {
62 return;
63 }
64
65 // time periods are specified as t<seconds>=<bitrate>
66 // e.g. #t15=450560&t150=65530
67 params = window.location.hash.substring(1)
68 .split('&')
69 .map(function(param) {
70 return ((/t(\d+)=(\d+)/i).exec(param) || [])
71 .map(window.parseFloat).slice(1);
72 }).filter(function(pair) {
73 return pair.length === 2;
74 });
75
76 networkTimeline.innerHTML = '';
77 params.forEach(function(param) {
78 appendTimePeriod();
79 networkTimeline.querySelector('li:last-child .time').value = param[0];
80 networkTimeline.querySelector('li:last-child input.bandwidth').value = param[1];
81 });
82 })();
83
84 // collect the simulation parameters
85 parameters = function() {
86 var times = Array.prototype.slice.call(document.querySelectorAll('.time')),
87 bandwidths = document.querySelectorAll('input.bandwidth'),
88 playlists = Array.prototype.slice.call(document.querySelectorAll('input.bitrate'));
89
90 return {
91 playlists: playlists.map(function(input) {
92 return +input.value;
93 }),
94 bandwidths: times.reduce(function(conditions, time, i) {
95 return conditions.concat({
96 time: +time.value,
97 bandwidth: +bandwidths[i].value
98 });
99 }, [])
100 };
101 };
102
103 // send a mock playlist response
104 playlistResponse = function(bitrate) {
105 var i = segmentCount,
106 response = '#EXTM3U\n';
107
108 while (i--) {
109 response += '#EXTINF:' + segmentDuration + ',\n';
110 response += bitrate + '-' + (segmentCount - i) + '\n';
111 }
112 response += '#EXT-X-ENDLIST\n';
113
114 return response;
115 };
116
117 // run the simulation
118 runSimulation = function(options, done) {
119 var results = {
120 bandwidth: [],
121 effectiveBandwidth: [],
122 playlists: [],
123 buffered: [],
124 options: options
125 },
126 bandwidths = options.bandwidths,
127 fixture = document.getElementById('fixture'),
128
129 realSetTimeout = window.setTimeout,
130 clock,
131 fakeXhr,
132 requests,
133 video,
134 t = 0,
135 i = 0;
136
137 // clean up the last run if necessary
138 if (player) {
139 player.dispose();
140 };
141
142 // mock out the environment
143 clock = sinon.useFakeTimers();
144 fakeXhr = sinon.useFakeXMLHttpRequest();
145 videojs.xhr.XMLHttpRequest = fakeXhr;
146 requests = [];
147 fakeXhr.onCreate = function(xhr) {
148 xhr.startTime = +new Date();
149 xhr.delivered = 0;
150 requests.push(xhr);
151 };
152
153 // initialize the HLS tech
154 fixture.innerHTML = '';
155 video = document.createElement('video');
156 video.className = 'video-js vjs-default-skin';
157 video.controls = true;
158 fixture.appendChild(video);
159 player = videojs(video, {
160 sources: [{
161 src: 'http://example.com/master.m3u8',
162 type: 'application/x-mpegurl'
163 }]
164 });
165
166 player.ready(function() {
167 // run next tick so that Flash doesn't swallow exceptions
168 realSetTimeout(function() {
169 var master = '#EXTM3U\n' +
170 options.playlists.reduce(function(playlists, value) {
171 return playlists +
172 '#EXT-X-STREAM-INF:BANDWIDTH=' + value + '\n' +
173 'playlist-' + value + '\n';
174 }, ''),
175 buffered = 0,
176 currentTime = 0;
177
178 // simulate buffered and currentTime during playback
179 player.buffered = function() {
180 return videojs.createTimeRange(0, currentTime + buffered);
181 };
182 player.currentTime = function() {
183 return currentTime;
184 };
185
186 bandwidths.sort(function(left, right) {
187 return left.time - right.time;
188 });
189
190 // respond to the playlist requests
191 requests[0].bandwidth = bandwidths[0].bandwidth;
192 requests.shift().respond(200, null, master);
193 requests[0].bandwidth = bandwidths[0].bandwidth;
194 requests[0].respond(200, null, playlistResponse(+requests[0].url.match(/\d+$/)));
195 requests.shift();
196
197 // record the measured bandwidth for the playlist requests
198 results.effectiveBandwidth.push({
199 time: 0,
200 bandwidth: player.hls.bandwidth
201 });
202
203 // pre-calculate the bandwidth at each second
204 for (t = i = 0; t < duration; t++) {
205 while (bandwidths[i + 1] && bandwidths[i + 1].time <= t) {
206 i++;
207 }
208 results.bandwidth.push({
209 time: t,
210 bandwidth: bandwidths[i].bandwidth
211 });
212 }
213
214 // advance time and collect simulation results
215 for (t = 0; t < duration; clock.tick(1000), t++) {
216 // schedule response deliveries
217 while (requests.length) {
218 (function(request) {
219 var segmentSize;
220
221 // playlist responses
222 if (/playlist-\d+$/.test(request.url)) {
223 // for simplicity, playlist responses have zero trasmission time
224 return setTimeout(function() {
225 request.respond(200, null, playlistResponse(+request.url.match(/\d+$/)));
226 }, propagationDelay * 1000);
227 }
228
229 // segment responses
230 segmentSize = +request.url.match(/(\d+)-\d+$/)[1] * segmentDuration;
231 // segment response headers arrive after the propogation delay
232 setTimeout(function() {
233 var arrival = Math.ceil(+new Date() * 0.001);
234 request.setResponseHeaders({
235 'Content-Type': 'video/mp2t'
236 });
237
238 results.bandwidth.slice(arrival).every(function(value, i) {
239 var remaining = segmentSize - request.delivered;
240 if (remaining - value.bandwidth <= 0) {
241 // send the response body once all bytes have been delivered
242 setTimeout(function() {
243 var time = Math.ceil(+new Date() * 0.001);
244 if (request.aborted) {
245 return;
246 }
247 request.status = 200;
248 request.response = new Uint8Array(segmentSize * 0.125);
249 request.setResponseBody('');
250
251 results.playlists.push({
252 time: time,
253 bitrate: +request.url.match(/(\d+)-\d+$/)[1]
254 });
255 // update the buffered value
256 buffered += segmentDuration;
257 results.buffered[results.buffered.length - 1].buffered = buffered;
258 results.effectiveBandwidth.push({
259 time: time,
260 bandwidth: player.hls.bandwidth
261 });
262 }, ((remaining / value.bandwidth) + i) * 1000);
263 return false;
264 }
265 // record the bits for this tick
266 request.delivered += value.bandwidth;
267 return true;
268 });
269 }, propagationDelay * 1000);
270 })(requests.shift());
271 }
272
273 results.buffered.push({
274 time: t,
275 buffered: buffered
276 });
277
278 // simulate playback
279 if (buffered > 0) {
280 buffered--;
281 currentTime++;
282 }
283 player.trigger('timeupdate');
284 }
285
286 // restore the environment
287 clock.restore();
288 fakeXhr.restore();
289
290 // update the fragment identifier so this scenario can be re-run easily
291 window.location.hash = '#' + options.bandwidths.map(function(interval) {
292 return 't' + interval.time + '=' + interval.bandwidth;
293 }).join('&');
294
295 done(null, results);
296 }, 0);
297 });
298 /// trigger the ready function through set timeout
299 clock.tick(1);
300 };
301 runButton = document.getElementById('run-simulation');
302 runButton.addEventListener('click', function() {
303 runSimulation(parameters(), displayTimeline);
304 });
305
306 // render the timeline with d3
307 timeline = document.querySelector('.timeline');
308 timeline.innerHTML = '';
309 (function() {
310 var margin = {
311 top: 20,
312 right: 80,
313 bottom: 30,
314 left: 50
315 },
316 width = 960 - margin.left - margin.right,
317 height = 500 - margin.top - margin.bottom,
318 svg;
319 svg = d3.select('.timeline').append('svg')
320 .attr('width', width + margin.left + margin.right)
321 .attr('height', height + margin.top + margin.bottom)
322 .append('g')
323 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
324
325 displayTimeline = function(error, data) {
326 var x = d3.scale.linear().range([0, width]),
327 y = d3.scale.linear().range([height, 0]),
328
329 timeAxis = d3.svg.axis().scale(x).orient('bottom'),
330 tickFormatter = d3.format(',.0f'),
331 bitrateAxis = d3.svg.axis()
332 .scale(y)
333 .tickFormat(function(value) {
334 return tickFormatter(value / 1024);
335 })
336 .orient('left'),
337
338 bandwidthLine = d3.svg.line()
339 .interpolate('basis')
340 .x(function(data) {
341 return x(data.time);
342 })
343 .y(function(data) {
344 return y(data.bandwidth);
345 }),
346 effectiveBandwidthLine = d3.svg.line()
347 .interpolate('basis')
348 .x(function(data) {
349 return x(data.time);
350 })
351 .y(function(data) {
352 return y(data.bandwidth);
353 });
354
355 x.domain(d3.extent(data.bandwidth, function(data) {
356 return data.time;
357 }));
358 y.domain([0, Math.max(d3.max(data.bandwidth, function(data) {
359 return data.bandwidth;
360 }), d3.max(data.options.playlists), d3.max(data.playlists, function(data) {
361 return data.bitrate;
362 }))]);
363
364 // time axis
365 svg.selectAll('.axis').remove();
366 svg.append('g')
367 .attr('class', 'x axis')
368 .attr('transform', 'translate(0,' + height + ')')
369 .call(timeAxis);
370
371 // bitrate axis
372 svg.append('g')
373 .attr('class', 'y axis')
374 .call(bitrateAxis)
375 .append('text')
376 .attr('transform', 'rotate(-90)')
377 .attr('y', 6)
378 .attr('dy', '.71em')
379 .style('text-anchor', 'end')
380 .text('Bitrate (kb/s)');
381
382 // playlist bitrate lines
383 svg.selectAll('.line.bitrate').remove();
384 svg.selectAll('.line.bitrate')
385 .data(data.options.playlists)
386 .enter().append('path')
387 .attr('class', 'line bitrate')
388 .attr('d', function(playlist) {
389 return 'M0,' + y(playlist) + 'L' + width + ',' + y(playlist);
390 });
391
392 // bandwidth line
393 svg.selectAll('.bandwidth').remove();
394 svg.append('path')
395 .datum(data.bandwidth)
396 .attr('class', 'line bandwidth')
397 .attr('d', bandwidthLine);
398 svg.selectAll('.effective-bandwidth').remove();
399 svg.append('path')
400 .datum(data.effectiveBandwidth)
401 .attr('class', 'line effective-bandwidth')
402 .attr('d', effectiveBandwidthLine);
403
404 svg.append('text')
405 .attr('class', 'bandwidth label')
406 .attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.bandwidth.slice(-1)[0].bandwidth) + ')')
407 .attr('dy', '1.35em')
408 .text('bandwidth');
409 svg.append('text')
410 .attr('class', 'bandwidth label')
411 .attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.effectiveBandwidth.slice(-1)[0].bandwidth) + ')')
412 .attr('dy', '1.35em')
413 .text('measured');
414
415 // segment bitrate dots
416 svg.selectAll('.segment-bitrate').remove();
417 svg.selectAll('.segment-bitrate')
418 .data(data.playlists)
419 .enter().append('circle')
420 .attr('class', 'dot segment-bitrate')
421 .attr('r', 3.5)
422 .attr('cx', function(playlist) {
423 return x(playlist.time);
424 })
425 .attr('cy', function(playlist) {
426 return y(playlist.bitrate);
427 });
428
429 // highlight intervals when the buffer is empty
430 svg.selectAll('.buffer-empty').remove();
431 svg.selectAll('.buffer-empty')
432 .data(data.buffered.reduce(function(result, sample) {
433 var last = result[result.length - 1];
434 if (sample.buffered === 0) {
435 if (last && sample.time === last.end + 1) {
436 // add this sample to the interval we're accumulating
437 return result.slice(0, result.length - 1).concat({
438 start: last.start,
439 end: sample.time
440 });
441 } else {
442 // this sample starts a new interval
443 return result.concat({
444 start: sample.time,
445 end: sample.time
446 });
447 }
448 }
449 // filter out time periods where the buffer isn't empty
450 return result;
451 }, []))
452 .enter().append('rect')
453 .attr('class', 'buffer-empty')
454 .attr('x', function(data) {
455 return x(data.start);
456 })
457 .attr('width', function(data) {
458 return x(1 + data.end - data.start);
459 })
460 .attr('y', 0)
461 .attr('height', y(height));
462 };
463 })();
464
465 runSimulation(parameters(), displayTimeline);
466
467 })(window, document);
1 Copyright (c) 2010-2014, Michael Bostock
2 All rights reserved.
3
4 Redistribution and use in source and binary forms, with or without
5 modification, are permitted provided that the following conditions are met:
6
7 * Redistributions of source code must retain the above copyright notice, this
8 list of conditions and the following disclaimer.
9
10 * Redistributions in binary form must reproduce the above copyright notice,
11 this list of conditions and the following disclaimer in the documentation
12 and/or other materials provided with the distribution.
13
14 * The name Michael Bostock may not be used to endorse or promote products
15 derived from this software without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
21 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
22 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
24 OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
26 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
No preview for this file type
1 /* Modernizr 2.6.2 (Custom Build) | MIT & BSD
2 * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load
3 */
4 ;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d<e;d++)u[c[d]]=c[d]in k;return u.list&&(u.list=!!b.createElement("datalist")&&!!a.HTMLDataListElement),u}("autocomplete autofocus list placeholder max min multiple pattern required step".split(" ")),e.inputtypes=function(a){for(var d=0,e,f,h,i=a.length;d<i;d++)k.setAttribute("type",f=a[d]),e=k.type!=="text",e&&(k.value=l,k.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(f)&&k.style.WebkitAppearance!==c?(g.appendChild(k),h=b.defaultView,e=h.getComputedStyle&&h.getComputedStyle(k,null).WebkitAppearance!=="textfield"&&k.offsetHeight!==0,g.removeChild(k)):/^(search|tel)$/.test(f)||(/^(url|email)$/.test(f)?e=k.checkValidity&&k.checkValidity()===!1:e=k.value!=l)),t[a[d]]=!!e;return t}("search tel url email datetime date month week time datetime-local number range color".split(" "))}var d="2.6.2",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k=b.createElement("input"),l=":)",m={}.toString,n=" -webkit- -moz- -o- -ms- ".split(" "),o="Webkit Moz O ms",p=o.split(" "),q=o.toLowerCase().split(" "),r={svg:"http://www.w3.org/2000/svg"},s={},t={},u={},v=[],w=v.slice,x,y=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["&#173;",'<style id="s',h,'">',a,"</style>"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="<svg/>",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e<g;e++)d.createElement(f[e]);return d}function p(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return r.shivMethods?n(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+l().join().replace(/\w+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(r,b.frag)}function q(a){a||(a=b);var c=m(a);return r.shivCSS&&!f&&!c.hasCSS&&(c.hasCSS=!!k(a,"article,aside,figcaption,figure,footer,header,hgroup,nav,section{display:block}mark{background:#FF0;color:#000}")),j||p(a,c),a}var c=a.html5||{},d=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,e=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,f,g="_html5shiv",h=0,i={},j;(function(){try{var a=b.createElement("a");a.innerHTML="<xyz></xyz>",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f<d;f++)g=a[f].split("="),(e=z[g.shift()])&&(c=e(c,g));for(f=0;f<b;f++)c=x[f](c);return c}function g(a,e,f,g,h){var i=b(a),j=i.autoCallback;i.url.split(".").pop().split("?").shift(),i.bypass||(e&&(e=d(e)?e:e[a]||e[g]||e[a.split("/").pop().split("?")[0]]),i.instead?i.instead(a,e,f,g,h):(y[i.url]?i.noexec=!0:y[i.url]=1,f.load(i.url,i.forceCSS||!i.forceJS&&"css"==i.url.split(".").pop().split("?").shift()?"c":c,i.noexec,i.attrs,i.timeout),(d(e)||d(j))&&f.load(function(){k(),e&&e(i.origUrl,h,g),j&&j(i.origUrl,h,g),y[i.url]=2})))}function h(a,b){function c(a,c){if(a){if(e(a))c||(j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}),g(a,j,b,0,h);else if(Object(a)===a)for(n in m=function(){var b=0,c;for(c in a)a.hasOwnProperty(c)&&b++;return b}(),a)a.hasOwnProperty(n)&&(!c&&!--m&&(d(j)?j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}:j[n]=function(a){return function(){var b=[].slice.call(arguments);a&&a.apply(this,b),l()}}(k[n])),g(a[n],j,b,n,h))}else!c&&l()}var h=!!a.test,i=a.load||a.both,j=a.callback||f,k=j,l=a.complete||f,m,n;c(h?a.yep:a.nope,!!i),i&&c(i)}var i,j,l=this.yepnope.loader;if(e(a))g(a,0,l,0);else if(w(a))for(i=0;i<a.length;i++)j=a[i],e(j)?g(j,0,l,0):w(j)?B(j):Object(j)===j&&h(j,l);else Object(a)===a&&h(a,l)},B.addPrefix=function(a,b){z[a]=b},B.addFilter=function(a){x.push(a)},B.errorTimeout=1e4,null==b.readyState&&b.addEventListener&&(b.readyState="loading",b.addEventListener("DOMContentLoaded",A=function(){b.removeEventListener("DOMContentLoaded",A,0),b.readyState="complete"},0)),a.yepnope=k(),a.yepnope.executeStack=h,a.yepnope.injectJs=function(a,c,d,e,i,j){var k=b.createElement("script"),l,o,e=e||B.errorTimeout;k.src=a;for(o in d)k.setAttribute(o,d[o]);c=j?h:c||f,k.onreadystatechange=k.onload=function(){!l&&g(k.readyState)&&(l=1,c(),k.onload=k.onreadystatechange=null)},m(function(){l||(l=1,c(1))},e),i?k.onload():n.parentNode.insertBefore(k,n)},a.yepnope.injectCss=function(a,c,d,e,g,i){var e=b.createElement("link"),j,c=i?h:c||f;e.href=a,e.rel="stylesheet",e.type="text/css";for(j in d)e.setAttribute(j,d[j]);g||(n.parentNode.insertBefore(e,n),m(c,0))}}(this,document),Modernizr.load=function(){yepnope.apply(window,[].slice.call(arguments,0))};
1 import {
2 useFakeEnvironment,
3 useFakeMediaSource,
4 createPlayer,
5 openMediaSource,
6 standardXHRResponse,
7 } from '../../test/test-helpers.js';
8
9 // the number of seconds of video in each segment
10 const segmentDuration = 9; // seconds
11
12 // the number of segments in the video
13 const segmentCount = 100;
14
15 // the length of the simulation
16 const duration = segmentDuration * segmentCount;
17
18 // the number of seconds it takes for a single bit to be
19 // transmitted from the client to the server, or vice-versa
20 const propagationDelay = 0.5;
21
22 // send a mock playlist response
23 const playlistResponse = function(request) {
24 let match = request.url.match(/\d+/);
25 let bitrate = match[0];
26
27 let i = segmentCount;
28 let response =
29 '#EXTM3U\n' +
30 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
31 '#EXT-X-TARGETDURATION:' + segmentDuration + '\n';
32
33 while (i--) {
34 response += '#EXTINF:' + segmentDuration + ',\n';
35 response += bitrate + '-' + (segmentCount - i) + '.ts\n';
36 }
37 response += '#EXT-X-ENDLIST\n';
38
39 return response;
40 };
41
42 // run the simulation
43 const runSimulation = function(options, done) {
44 // SETUP
45 let results = {
46 bandwidth: [],
47 effectiveBandwidth: [],
48 playlists: [],
49 buffered: [],
50 options: options
51 };
52 let t = 0;
53 let i = 0;
54 let env = useFakeEnvironment();
55 let clock = env.clock;
56 let requests = env.requests;
57 let mse = useFakeMediaSource();
58 let buffered = 0;
59 let currentTime = 0;
60 let player = window.player = createPlayer();
61
62 document.querySelector('#qunit-fixture').style = 'display: none;';
63 player.src({
64 src: 'http://example.com/master.m3u8',
65 type: 'application/x-mpegurl'
66 });
67 openMediaSource(player, clock);
68
69 // run next tick so that Flash doesn't swallow exceptions
70 let master = '#EXTM3U\n';
71 options.playlists.forEach((bandwidth) => {
72 master+= '#EXT-X-STREAM-INF:BANDWIDTH=' + bandwidth + '\n';
73 master += 'playlist-' + bandwidth + '.m3u8\n';
74 });
75
76 // simulate buffered and currentTime during playback
77 let getBuffer = (buff) => {
78 return videojs.createTimeRange(0, currentTime + buffered);
79 };
80 player.tech_.buffered = getBuffer;
81
82 Object.defineProperty(player.tech_, 'time_', {
83 get: () => currentTime
84 });
85
86 options.bandwidths.sort(function(left, right) {
87 return left.time - right.time;
88 });
89
90 // respond to the playlist requests
91 let masterRequest = requests.shift();
92 masterRequest.bandwidth = options.bandwidths[0].bandwidth;
93 masterRequest.respond(200, null, master);
94
95 let playlistRequest = requests.shift();
96 playlistRequest.bandwidth = options.bandwidths[0].bandwidth;
97 playlistRequest.respond(200, null, playlistResponse(playlistRequest));
98
99 let sourceBuffer = player.tech_.hls.mediaSource.sourceBuffers[0];
100 Object.defineProperty(sourceBuffer, 'buffered', {
101 get: () => buffered
102 });
103
104 // record the measured bandwidth for the playlist requests
105 results.effectiveBandwidth.push({
106 time: 0,
107 bandwidth: player.tech_.hls.bandwidth
108 });
109
110 // advance time and collect simulation results
111 for (t = i = 0; t < duration; clock.tick(1000), t++) {
112 while (options.bandwidths[i + 1] && options.bandwidths[i + 1].time <= t) {
113 i++;
114 }
115 let bandwidth = options.bandwidths[i].bandwidth;
116 results.bandwidth.push({
117 time: t,
118 bandwidth: bandwidth
119 });
120
121 // schedule response deliveries
122 while (requests.length) {
123 let request = requests.shift();
124 request.bandwidth = bandwidth;
125
126 // playlist responses
127 if (/\.m3u8$/.test(request.url)) {
128 // for simplicity, playlist responses have zero trasmission time
129 request.respond(200, null, playlistResponse(request));
130 continue;
131 }
132
133 // segment responses
134 let segmentSize = request.url.match(/(\d+)-\d+/)[1] * segmentDuration;
135
136 //console.log(segmentSize);
137 //console.log(bandwidth);
138 console.log(request.url);
139 let timeToTake = segmentSize/bandwidth + (propagationDelay * 1);
140
141 setTimeout(() => {
142 if (request.aborted) {
143 console.error("Request for segment aborted, download timedout")
144 return;
145 }
146
147 request.response = new Uint8Array(segmentSize * 0.125);
148 request.respond(200, null, '');
149 sourceBuffer.trigger('updateend');
150
151 results.playlists.push({
152 time: t,
153 bitrate: +request.url.match(/(\d+)-\d+/)[1]
154 });
155
156 buffered += segmentDuration;
157 results.effectiveBandwidth.push({
158 time: t,
159 bandwidth: player.tech_.hls.bandwidth
160 });
161 }, timeToTake * 1000);
162 // console.log(`taking ${timeToTake}s for response`);
163 }
164
165 results.buffered.push({
166 time: t,
167 buffered: buffered
168 });
169
170 // simulate playback
171 if (buffered > 0) {
172 buffered--;
173 currentTime++;
174 }
175 player.trigger('timeupdate');
176 }
177
178 // update the fragment identifier so this scenario can be re-run easily
179 window.location.hash = '#' + options.bandwidths.map(function(interval) {
180 return 't' + interval.time + '=' + interval.bandwidth;
181 }).join('&');
182
183 player.dispose();
184 mse.restore();
185 env.restore();
186
187 console.log(results);
188 done(null, results);
189 };
190
191 export default runSimulation;
1 import runSimulation from './run-simulation';
2 import displayTimeline from './display-timeline';
3
4
5
6 // a dynamic number of time-bandwidth pairs may be defined to drive the simulation
7 let networkTimeline = document.querySelector('.network-timeline');
8 let timePeriod = networkTimeline.querySelector('li:last-child').cloneNode(true);
9 const appendTimePeriod = function() {
10 let clone = timePeriod.cloneNode(true);
11 let count = networkTimeline.querySelectorAll('input.bandwidth').length;
12 let time = clone.querySelector('.time');
13 let bandwidth = clone.querySelector('input.bandwidth');
14
15 time.name = 'time' + count;
16 bandwidth.name = 'bandwidth' + count;
17 networkTimeline.appendChild(clone);
18 };
19 document.querySelector('.add-time-period').addEventListener('click', appendTimePeriod);
20
21 // apply any simulation parameters that were set in the fragment identifier
22 if (window.location.hash) {
23 // time periods are specified as t<seconds>=<bitrate>
24 // e.g. #t15=450560&t150=65530
25 let params = window.location.hash.substring(1)
26 .split('&')
27 .map(function(param) {
28 return ((/t(\d+)=(\d+)/i).exec(param) || [])
29 .map(window.parseFloat).slice(1);
30 }).filter(function(pair) {
31 return pair.length === 2;
32 });
33
34 networkTimeline.innerHTML = '';
35 params.forEach(function(param) {
36 appendTimePeriod();
37 networkTimeline.querySelector('li:last-child .time').value = param[0];
38 networkTimeline.querySelector('li:last-child input.bandwidth').value = param[1];
39 });
40 }
41
42 // collect the simulation parameters
43 const parameters = function() {
44 let times = Array.prototype.slice.call(document.querySelectorAll('.time'));
45 let bandwidths = document.querySelectorAll('input.bandwidth');
46 let playlists = Array.prototype.slice.call(document.querySelectorAll('input.bitrate'));
47
48 return {
49 playlists: playlists.map(function(input) {
50 return +input.value;
51 }),
52 bandwidths: times.reduce(function(conditions, time, i) {
53 return conditions.concat({
54 time: +time.value,
55 bandwidth: +bandwidths[i].value
56 });
57 }, [])
58 };
59 };
60
61 let runButton = document.getElementById('run-simulation');
62 runButton.addEventListener('click', function() {
63 runSimulation(parameters(), displayTimeline);
64 });
65
66 runButton.click();