73540062 by Brandon Casey Committed by GitHub

got the netowrk-switcher working (#766)

1 parent 1837d8d9
......@@ -40,9 +40,10 @@
"version": "npm run build",
"watch": "npm-run-all -p watch:*",
"watch:docs": "nodemon --watch src/ --exec npm run docs",
"watch:js": "npm-run-all -p watch:js:babel watch:js:browserify",
"watch:js": "npm-run-all -p watch:js:babel watch:js:browserify watch:js:switcher",
"watch:js:babel": "npm run build:js:babel -- --watch",
"watch:js:browserify": "watchify . -v -g browserify-shim -o dist/videojs-contrib-hls.js",
"watch:js:switcher": "watchify utils/switcher/switcher.js -v -t babelify -g browserify-shim -o dist/switcher.js",
"watch:test": "npm-run-all -p watch:test:*",
"watch:test:js": "node scripts/watch-test.js",
"watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
......@@ -98,6 +99,7 @@
"browserify-shim": "^3.0.0",
"connect": "^3.4.0",
"cowsay": "^1.1.0",
"d3": "3.4.8",
"doctoc": "^0.15.0",
"glob": "^6.0.3",
"jsdoc": "^3.4.0",
......
......@@ -117,7 +117,9 @@ let fakeEnvironment = {
this.xhr.restore();
['warn', 'error'].forEach((level) => {
if (this.log && this.log[level] && this.log[level].restore) {
if (QUnit) {
QUnit.equal(this.log[level].callCount, 0, `no unexpected logs on ${level}`);
}
this.log[level].restore();
}
});
......@@ -256,7 +258,7 @@ export const openMediaSource = function(player, clock) {
});
};
export const standardXHRResponse = function(request) {
export const standardXHRResponse = function(request, data) {
if (!request.url) {
return;
}
......@@ -277,9 +279,12 @@ export const standardXHRResponse = function(request) {
contentType = 'video/MP2T';
}
if (!data) {
data = testDataManifests[manifestName];
}
request.response = new Uint8Array(16).buffer;
request.respond(200, { 'Content-Type': contentType },
testDataManifests[manifestName]);
request.respond(200, {'Content-Type': contentType}, data);
};
// return an absolute version of a page-relative URL
......
/*! normalize.css v1.1.2 | MIT License | git.io/normalize */
/* ==========================================================================
HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
display: block;
}
/**
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
* Known issue: no IE 6 support.
*/
[hidden] {
display: none;
}
/* ==========================================================================
Base
========================================================================== */
/**
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
* `em` units.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-size: 100%; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Address `font-family` inconsistency between `textarea` and other form
* elements.
*/
html,
button,
input,
select,
textarea {
font-family: sans-serif;
}
/**
* Address margins handled incorrectly in IE 6/7.
*/
body {
margin: 0;
}
/* ==========================================================================
Links
========================================================================== */
/**
* Address `outline` inconsistency between Chrome and other browsers.
*/
a:focus {
outline: thin dotted;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* ==========================================================================
Typography
========================================================================== */
/**
* Address font sizes and margins set differently in IE 6/7.
* Address font sizes within `section` and `article` in Firefox 4+, Safari 5,
* and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: 0.83em 0;
}
h3 {
font-size: 1.17em;
margin: 1em 0;
}
h4 {
font-size: 1em;
margin: 1.33em 0;
}
h5 {
font-size: 0.83em;
margin: 1.67em 0;
}
h6 {
font-size: 0.67em;
margin: 2.33em 0;
}
/**
* Address styling not present in IE 7/8/9, Safari 5, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome.
*/
b,
strong {
font-weight: bold;
}
blockquote {
margin: 1em 40px;
}
/**
* Address styling not present in Safari 5 and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address differences between Firefox and other browsers.
* Known issue: no IE 6/7 normalization.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Address styling not present in IE 6/7/8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address margins set differently in IE 6/7.
*/
p,
pre {
margin: 1em 0;
}
/**
* Correct font family set oddly in IE 6, Safari 4/5, and Chrome.
*/
code,
kbd,
pre,
samp {
font-family: monospace, serif;
_font-family: 'courier new', monospace;
font-size: 1em;
}
/**
* Improve readability of pre-formatted text in all browsers.
*/
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
/**
* Address CSS quotes not supported in IE 6/7.
*/
q {
quotes: none;
}
/**
* Address `quotes` property not supported in Safari 4.
*/
q:before,
q:after {
content: '';
content: none;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* ==========================================================================
Lists
========================================================================== */
/**
* Address margins set differently in IE 6/7.
*/
dl,
menu,
ol,
ul {
margin: 1em 0;
}
dd {
margin: 0 0 0 40px;
}
/**
* Address paddings set differently in IE 6/7.
*/
menu,
ol,
ul {
padding: 0 0 0 40px;
}
/**
* Correct list images handled incorrectly in IE 7.
*/
nav ul,
nav ol {
list-style: none;
list-style-image: none;
}
/* ==========================================================================
Embedded content
========================================================================== */
/**
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
* 2. Improve image quality when scaled in IE 7.
*/
img {
border: 0; /* 1 */
-ms-interpolation-mode: bicubic; /* 2 */
}
/**
* Correct overflow displayed oddly in IE 9.
*/
svg:not(:root) {
overflow: hidden;
}
/* ==========================================================================
Figures
========================================================================== */
/**
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
*/
figure {
margin: 0;
}
/* ==========================================================================
Forms
========================================================================== */
/**
* Correct margin displayed oddly in IE 6/7.
*/
form {
margin: 0;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct color not being inherited in IE 6/7/8/9.
* 2. Correct text not wrapping in Firefox 3.
* 3. Correct alignment displayed oddly in IE 6/7.
*/
legend {
border: 0; /* 1 */
padding: 0;
white-space: normal; /* 2 */
*margin-left: -7px; /* 3 */
}
/**
* 1. Correct font size not being inherited in all browsers.
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
* and Chrome.
* 3. Improve appearance and consistency in all browsers.
*/
button,
input,
select,
textarea {
font-size: 100%; /* 1 */
margin: 0; /* 2 */
vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
}
/**
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
* Correct `select` style inheritance in Firefox 4+ and Opera.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
* Known issue: inner spacing remains in IE 6.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
*overflow: visible; /* 4 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* 1. Address box sizing set to content-box in IE 8/9.
* 2. Remove excess padding in IE 8/9.
* 3. Remove excess padding in IE 7.
* Known issue: excess padding remains in IE 6.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
*height: 13px; /* 3 */
*width: 13px; /* 3 */
}
/**
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Remove inner padding and border in Firefox 3+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
* 2. Improve readability and alignment in all browsers.
*/
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/* ==========================================================================
Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
/*! 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
// render the timeline with d3
let timeline = document.querySelector('.timeline');
timeline.innerHTML = '';
let margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
};
let width = 960 - margin.left - margin.right;
let height = 500 - margin.top - margin.bottom;
let svg = d3.select('.timeline').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
const displayTimeline = function(error, data) {
var x = d3.scale.linear().range([0, width]),
y = d3.scale.linear().range([height, 0]),
timeAxis = d3.svg.axis().scale(x).orient('bottom'),
tickFormatter = d3.format(',.0f'),
bitrateAxis = d3.svg.axis()
.scale(y)
.tickFormat(function(value) {
return tickFormatter(value / 1024);
})
.orient('left'),
bandwidthLine = d3.svg.line()
.interpolate('basis')
.x(function(data) {
return x(data.time);
})
.y(function(data) {
return y(data.bandwidth);
}),
effectiveBandwidthLine = d3.svg.line()
.interpolate('basis')
.x(function(data) {
return x(data.time);
})
.y(function(data) {
return y(data.bandwidth);
});
x.domain(d3.extent(data.bandwidth, function(data) {
return data.time;
}));
y.domain([0, Math.max(d3.max(data.bandwidth, function(data) {
return data.bandwidth;
}), d3.max(data.options.playlists), d3.max(data.playlists, function(data) {
return data.bitrate;
}))]);
// time axis
svg.selectAll('.axis').remove();
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(timeAxis);
// bitrate axis
svg.append('g')
.attr('class', 'y axis')
.call(bitrateAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end')
.text('Bitrate (kb/s)');
// playlist bitrate lines
svg.selectAll('.line.bitrate').remove();
svg.selectAll('.line.bitrate')
.data(data.options.playlists)
.enter().append('path')
.attr('class', 'line bitrate')
.attr('d', function(playlist) {
return 'M0,' + y(playlist) + 'L' + width + ',' + y(playlist);
});
// bandwidth line
svg.selectAll('.bandwidth').remove();
svg.append('path')
.datum(data.bandwidth)
.attr('class', 'line bandwidth')
.attr('d', bandwidthLine);
svg.selectAll('.effective-bandwidth').remove();
svg.append('path')
.datum(data.effectiveBandwidth)
.attr('class', 'line effective-bandwidth')
.attr('d', effectiveBandwidthLine);
svg.append('text')
.attr('class', 'bandwidth label')
.attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.bandwidth.slice(-1)[0].bandwidth) + ')')
.attr('dy', '1.35em')
.text('bandwidth');
svg.append('text')
.attr('class', 'bandwidth label')
.attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.effectiveBandwidth.slice(-1)[0].bandwidth) + ')')
.attr('dy', '1.35em')
.text('measured');
// segment bitrate dots
svg.selectAll('.segment-bitrate').remove();
svg.selectAll('.segment-bitrate')
.data(data.playlists)
.enter().append('circle')
.attr('class', 'dot segment-bitrate')
.attr('r', 3.5)
.attr('cx', function(playlist) {
return x(playlist.time);
})
.attr('cy', function(playlist) {
return y(playlist.bitrate);
});
// highlight intervals when the buffer is empty
svg.selectAll('.buffer-empty').remove();
svg.selectAll('.buffer-empty')
.data(data.buffered.reduce(function(result, sample) {
var last = result[result.length - 1];
if (sample.buffered === 0) {
if (last && sample.time === last.end + 1) {
// add this sample to the interval we're accumulating
return result.slice(0, result.length - 1).concat({
start: last.start,
end: sample.time
});
} else {
// this sample starts a new interval
return result.concat({
start: sample.time,
end: sample.time
});
}
}
// filter out time periods where the buffer isn't empty
return result;
}, []))
.enter().append('rect')
.attr('class', 'buffer-empty')
.attr('x', function(data) {
return x(data.start);
})
.attr('width', function(data) {
return x(1 + data.end - data.start);
})
.attr('y', 0)
.attr('height', y(height));
};
export default displayTimeline;
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Playlist Switching Simulator</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="css/normalize.min.css">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="../../node_modules/video.js/dist/video-js.css">
<script src="js/vendor/modernizr-2.6.2.min.js"></script>
<link rel="stylesheet" href="switcher.css">
<link rel="stylesheet" href="/node_modules/video.js/dist/video-js.css">
</head>
<body>
<!--[if lt IE 7]>
<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>
<![endif]-->
<div class="header-container">
<header class="wrapper clearfix">
<h1 class="title">Playlist Switching Simulator</h1>
......@@ -29,7 +14,6 @@
<div class="main-container">
<div class="main wrapper clearfix">
<article>
<header>
<p>
......@@ -38,11 +22,6 @@
can helpful to understand how tweaks to the switching
logic will affect playback.
</p>
<p>
Flash security restrictions prevent this page from running
over the file protocol. Run <code>grunt connect</code> and
then <a href="http://localhost:9999/test/switcher">reload.</a>
</p>
</header>
<section>
<h2>Timeline</h2>
......@@ -58,7 +37,7 @@
After <input name=time0 class=time type=number value=0 min=0> seconds,
</label>
<label>
the link capacity is <input name=bandwidth0 class=bandwidth type=number value=921600> bits per second
the link capacity is <input name=bandwidth0 class=bandwidth type=number value=250000> bits per second
</label>
</li>
<li>
......@@ -66,7 +45,7 @@
After <input name=time1 class=time type=number value=150 min=0> seconds,
</label>
<label>
the link capacity is <input name=bandwidth1 class=bandwidth type=number value=450560> bits per second
the link capacity is <input name=bandwidth1 class=bandwidth type=number value=500000> bits per second
</label>
</li>
<li>
......@@ -74,7 +53,7 @@
After <input name=time2 class=time type=number value=600 min=0> seconds,
</label>
<label>
the link capacity is <input name=bandwidth2 class=bandwidth type=number value=1843200> bits per second
the link capacity is <input name=bandwidth2 class=bandwidth type=number value=250000> bits per second
</label>
</li>
</ol>
......@@ -94,44 +73,28 @@
</p>
The video is available at
<ul>
<li><input class=bitrate type=number min=1 value=312000> bits per second</li>
<li><input class=bitrate type=number min=1 value=524000> bits per second</li>
<li><input class=bitrate type=number min=1 value=1296000> bits per second</li>
<li><input class=bitrate type=number min=1 value=2125000> bits per second</li>
<li><input class=bitrate type=number min=1 value=3125000> bits per second</li>
<li><input class=bitrate type=number min=1 value=52400> bits per second</li>
<li><input class=bitrate type=number min=1 value=129600> bits per second</li>
<li><input class=bitrate type=number min=1 value=212500> bits per second</li>
<li><input class=bitrate type=number min=1 value=312500> bits per second</li>
</ul>
</form>
</section>
</article>
</div> <!-- #main -->
</div> <!-- #main-container -->
</div>
</div>
<div class="footer-container">
<footer class="wrapper">
<h3>videojs-contrib-hls</h3>
</footer>
</div>
<div id=fixture></div>
<script src="../../node_modules/sinon/lib/sinon.js"></script>
<script src="../../node_modules/sinon/lib/sinon/util/event.js"></script>
<script src="../../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js"></script>
<script src="../../node_modules/sinon/lib/sinon/util/xhr_ie.js"></script>
<script src="../../node_modules/sinon/lib/sinon/util/fake_timers.js"></script>
<script src="js/vendor/d3.min.js"></script>
<div id="qunit-fixture"></div>
<script src="/node_modules/sinon/pkg/sinon.js"></script>
<script src="/node_modules/video.js/dist/video.js"></script>
<script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
<script src="/node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="/src/videojs-hls.js"></script>
<script src="/src/xhr.js"></script>
<script src="/src/stream.js"></script>
<script src="/src/m3u8/m3u8-parser.js"></script>
<script src="/src/playlist.js"></script>
<script src="/src/playlist-loader.js"></script>
<script src="/src/decrypter.js"></script>
<script src="/src/bin-utils.js"></script>
<script src="js/switcher.js"></script>
<script src="/dist/videojs-contrib-hls.js"></script>
<script src="/node_modules/d3/d3.min.js"></script>
<script src="/dist/switcher.js"></script>
</body>
</html>
......
(function(window, document) {
'use strict';
// the number of seconds of video in each segment
var segmentDuration = 9, // seconds
// the number of segments in the video
segmentCount = 100,
// the length of the simulation
duration = segmentDuration * segmentCount,
// the number of seconds it takes for a single bit to be
// transmitted from the client to the server, or vice-versa
propagationDelay = 0.5,
runSimulation,
playlistResponse,
player,
runButton,
parameters,
timeline,
displayTimeline;
// mock out the environment and dependencies
videojs.options.flash.swf = '../../node_modules/video.js/dist/video-js/video-js.swf';
videojs.Hls.SegmentParser = function() {
this.getFlvHeader = function() {
return new Uint8Array([]);
};
this.parseSegmentBinaryData = function() {};
this.flushTags = function() {};
this.tagsAvailable = function() {
return false;
};
this.metadataStream = {
on: function() {}
};
};
// a dynamic number of time-bandwidth pairs may be defined to drive the simulation
(function() {
var params,
networkTimeline = document.querySelector('.network-timeline'),
timePeriod = networkTimeline.querySelector('li:last-child').cloneNode(true),
appendTimePeriod = function() {
var clone = timePeriod.cloneNode(true),
count = networkTimeline.querySelectorAll('input.bandwidth').length,
time = clone.querySelector('.time'),
bandwidth = clone.querySelector('input.bandwidth');
time.name = 'time' + count;
bandwidth.name = 'bandwidth' + count;
networkTimeline.appendChild(clone);
};
document.querySelector('.add-time-period')
.addEventListener('click', appendTimePeriod);
// apply any simulation parameters that were set in the fragment identifier
if (!window.location.hash) {
return;
}
// time periods are specified as t<seconds>=<bitrate>
// e.g. #t15=450560&t150=65530
params = window.location.hash.substring(1)
.split('&')
.map(function(param) {
return ((/t(\d+)=(\d+)/i).exec(param) || [])
.map(window.parseFloat).slice(1);
}).filter(function(pair) {
return pair.length === 2;
});
networkTimeline.innerHTML = '';
params.forEach(function(param) {
appendTimePeriod();
networkTimeline.querySelector('li:last-child .time').value = param[0];
networkTimeline.querySelector('li:last-child input.bandwidth').value = param[1];
});
})();
// collect the simulation parameters
parameters = function() {
var times = Array.prototype.slice.call(document.querySelectorAll('.time')),
bandwidths = document.querySelectorAll('input.bandwidth'),
playlists = Array.prototype.slice.call(document.querySelectorAll('input.bitrate'));
return {
playlists: playlists.map(function(input) {
return +input.value;
}),
bandwidths: times.reduce(function(conditions, time, i) {
return conditions.concat({
time: +time.value,
bandwidth: +bandwidths[i].value
});
}, [])
};
};
// send a mock playlist response
playlistResponse = function(bitrate) {
var i = segmentCount,
response = '#EXTM3U\n';
while (i--) {
response += '#EXTINF:' + segmentDuration + ',\n';
response += bitrate + '-' + (segmentCount - i) + '\n';
}
response += '#EXT-X-ENDLIST\n';
return response;
};
// run the simulation
runSimulation = function(options, done) {
var results = {
bandwidth: [],
effectiveBandwidth: [],
playlists: [],
buffered: [],
options: options
},
bandwidths = options.bandwidths,
fixture = document.getElementById('fixture'),
realSetTimeout = window.setTimeout,
clock,
fakeXhr,
requests,
video,
t = 0,
i = 0;
// clean up the last run if necessary
if (player) {
player.dispose();
};
// mock out the environment
clock = sinon.useFakeTimers();
fakeXhr = sinon.useFakeXMLHttpRequest();
videojs.xhr.XMLHttpRequest = fakeXhr;
requests = [];
fakeXhr.onCreate = function(xhr) {
xhr.startTime = +new Date();
xhr.delivered = 0;
requests.push(xhr);
};
// initialize the HLS tech
fixture.innerHTML = '';
video = document.createElement('video');
video.className = 'video-js vjs-default-skin';
video.controls = true;
fixture.appendChild(video);
player = videojs(video, {
sources: [{
src: 'http://example.com/master.m3u8',
type: 'application/x-mpegurl'
}]
});
player.ready(function() {
// run next tick so that Flash doesn't swallow exceptions
realSetTimeout(function() {
var master = '#EXTM3U\n' +
options.playlists.reduce(function(playlists, value) {
return playlists +
'#EXT-X-STREAM-INF:BANDWIDTH=' + value + '\n' +
'playlist-' + value + '\n';
}, ''),
buffered = 0,
currentTime = 0;
// simulate buffered and currentTime during playback
player.buffered = function() {
return videojs.createTimeRange(0, currentTime + buffered);
};
player.currentTime = function() {
return currentTime;
};
bandwidths.sort(function(left, right) {
return left.time - right.time;
});
// respond to the playlist requests
requests[0].bandwidth = bandwidths[0].bandwidth;
requests.shift().respond(200, null, master);
requests[0].bandwidth = bandwidths[0].bandwidth;
requests[0].respond(200, null, playlistResponse(+requests[0].url.match(/\d+$/)));
requests.shift();
// record the measured bandwidth for the playlist requests
results.effectiveBandwidth.push({
time: 0,
bandwidth: player.hls.bandwidth
});
// pre-calculate the bandwidth at each second
for (t = i = 0; t < duration; t++) {
while (bandwidths[i + 1] && bandwidths[i + 1].time <= t) {
i++;
}
results.bandwidth.push({
time: t,
bandwidth: bandwidths[i].bandwidth
});
}
// advance time and collect simulation results
for (t = 0; t < duration; clock.tick(1000), t++) {
// schedule response deliveries
while (requests.length) {
(function(request) {
var segmentSize;
// playlist responses
if (/playlist-\d+$/.test(request.url)) {
// for simplicity, playlist responses have zero trasmission time
return setTimeout(function() {
request.respond(200, null, playlistResponse(+request.url.match(/\d+$/)));
}, propagationDelay * 1000);
}
// segment responses
segmentSize = +request.url.match(/(\d+)-\d+$/)[1] * segmentDuration;
// segment response headers arrive after the propogation delay
setTimeout(function() {
var arrival = Math.ceil(+new Date() * 0.001);
request.setResponseHeaders({
'Content-Type': 'video/mp2t'
});
results.bandwidth.slice(arrival).every(function(value, i) {
var remaining = segmentSize - request.delivered;
if (remaining - value.bandwidth <= 0) {
// send the response body once all bytes have been delivered
setTimeout(function() {
var time = Math.ceil(+new Date() * 0.001);
if (request.aborted) {
return;
}
request.status = 200;
request.response = new Uint8Array(segmentSize * 0.125);
request.setResponseBody('');
results.playlists.push({
time: time,
bitrate: +request.url.match(/(\d+)-\d+$/)[1]
});
// update the buffered value
buffered += segmentDuration;
results.buffered[results.buffered.length - 1].buffered = buffered;
results.effectiveBandwidth.push({
time: time,
bandwidth: player.hls.bandwidth
});
}, ((remaining / value.bandwidth) + i) * 1000);
return false;
}
// record the bits for this tick
request.delivered += value.bandwidth;
return true;
});
}, propagationDelay * 1000);
})(requests.shift());
}
results.buffered.push({
time: t,
buffered: buffered
});
// simulate playback
if (buffered > 0) {
buffered--;
currentTime++;
}
player.trigger('timeupdate');
}
// restore the environment
clock.restore();
fakeXhr.restore();
// update the fragment identifier so this scenario can be re-run easily
window.location.hash = '#' + options.bandwidths.map(function(interval) {
return 't' + interval.time + '=' + interval.bandwidth;
}).join('&');
done(null, results);
}, 0);
});
/// trigger the ready function through set timeout
clock.tick(1);
};
runButton = document.getElementById('run-simulation');
runButton.addEventListener('click', function() {
runSimulation(parameters(), displayTimeline);
});
// render the timeline with d3
timeline = document.querySelector('.timeline');
timeline.innerHTML = '';
(function() {
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
svg;
svg = d3.select('.timeline').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
displayTimeline = function(error, data) {
var x = d3.scale.linear().range([0, width]),
y = d3.scale.linear().range([height, 0]),
timeAxis = d3.svg.axis().scale(x).orient('bottom'),
tickFormatter = d3.format(',.0f'),
bitrateAxis = d3.svg.axis()
.scale(y)
.tickFormat(function(value) {
return tickFormatter(value / 1024);
})
.orient('left'),
bandwidthLine = d3.svg.line()
.interpolate('basis')
.x(function(data) {
return x(data.time);
})
.y(function(data) {
return y(data.bandwidth);
}),
effectiveBandwidthLine = d3.svg.line()
.interpolate('basis')
.x(function(data) {
return x(data.time);
})
.y(function(data) {
return y(data.bandwidth);
});
x.domain(d3.extent(data.bandwidth, function(data) {
return data.time;
}));
y.domain([0, Math.max(d3.max(data.bandwidth, function(data) {
return data.bandwidth;
}), d3.max(data.options.playlists), d3.max(data.playlists, function(data) {
return data.bitrate;
}))]);
// time axis
svg.selectAll('.axis').remove();
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(timeAxis);
// bitrate axis
svg.append('g')
.attr('class', 'y axis')
.call(bitrateAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end')
.text('Bitrate (kb/s)');
// playlist bitrate lines
svg.selectAll('.line.bitrate').remove();
svg.selectAll('.line.bitrate')
.data(data.options.playlists)
.enter().append('path')
.attr('class', 'line bitrate')
.attr('d', function(playlist) {
return 'M0,' + y(playlist) + 'L' + width + ',' + y(playlist);
});
// bandwidth line
svg.selectAll('.bandwidth').remove();
svg.append('path')
.datum(data.bandwidth)
.attr('class', 'line bandwidth')
.attr('d', bandwidthLine);
svg.selectAll('.effective-bandwidth').remove();
svg.append('path')
.datum(data.effectiveBandwidth)
.attr('class', 'line effective-bandwidth')
.attr('d', effectiveBandwidthLine);
svg.append('text')
.attr('class', 'bandwidth label')
.attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.bandwidth.slice(-1)[0].bandwidth) + ')')
.attr('dy', '1.35em')
.text('bandwidth');
svg.append('text')
.attr('class', 'bandwidth label')
.attr('transform', 'translate(' + x(x.range()[1]) + ', ' + y(data.effectiveBandwidth.slice(-1)[0].bandwidth) + ')')
.attr('dy', '1.35em')
.text('measured');
// segment bitrate dots
svg.selectAll('.segment-bitrate').remove();
svg.selectAll('.segment-bitrate')
.data(data.playlists)
.enter().append('circle')
.attr('class', 'dot segment-bitrate')
.attr('r', 3.5)
.attr('cx', function(playlist) {
return x(playlist.time);
})
.attr('cy', function(playlist) {
return y(playlist.bitrate);
});
// highlight intervals when the buffer is empty
svg.selectAll('.buffer-empty').remove();
svg.selectAll('.buffer-empty')
.data(data.buffered.reduce(function(result, sample) {
var last = result[result.length - 1];
if (sample.buffered === 0) {
if (last && sample.time === last.end + 1) {
// add this sample to the interval we're accumulating
return result.slice(0, result.length - 1).concat({
start: last.start,
end: sample.time
});
} else {
// this sample starts a new interval
return result.concat({
start: sample.time,
end: sample.time
});
}
}
// filter out time periods where the buffer isn't empty
return result;
}, []))
.enter().append('rect')
.attr('class', 'buffer-empty')
.attr('x', function(data) {
return x(data.start);
})
.attr('width', function(data) {
return x(1 + data.end - data.start);
})
.attr('y', 0)
.attr('height', y(height));
};
})();
runSimulation(parameters(), displayTimeline);
})(window, document);
Copyright (c) 2010-2014, Michael Bostock
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name Michael Bostock may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
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
/* Modernizr 2.6.2 (Custom Build) | MIT & BSD
* 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
*/
;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))};
import {
useFakeEnvironment,
useFakeMediaSource,
createPlayer,
openMediaSource,
standardXHRResponse,
} from '../../test/test-helpers.js';
// the number of seconds of video in each segment
const segmentDuration = 9; // seconds
// the number of segments in the video
const segmentCount = 100;
// the length of the simulation
const duration = segmentDuration * segmentCount;
// the number of seconds it takes for a single bit to be
// transmitted from the client to the server, or vice-versa
const propagationDelay = 0.5;
// send a mock playlist response
const playlistResponse = function(request) {
let match = request.url.match(/\d+/);
let bitrate = match[0];
let i = segmentCount;
let response =
'#EXTM3U\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
'#EXT-X-TARGETDURATION:' + segmentDuration + '\n';
while (i--) {
response += '#EXTINF:' + segmentDuration + ',\n';
response += bitrate + '-' + (segmentCount - i) + '.ts\n';
}
response += '#EXT-X-ENDLIST\n';
return response;
};
// run the simulation
const runSimulation = function(options, done) {
// SETUP
let results = {
bandwidth: [],
effectiveBandwidth: [],
playlists: [],
buffered: [],
options: options
};
let t = 0;
let i = 0;
let env = useFakeEnvironment();
let clock = env.clock;
let requests = env.requests;
let mse = useFakeMediaSource();
let buffered = 0;
let currentTime = 0;
let player = window.player = createPlayer();
document.querySelector('#qunit-fixture').style = 'display: none;';
player.src({
src: 'http://example.com/master.m3u8',
type: 'application/x-mpegurl'
});
openMediaSource(player, clock);
// run next tick so that Flash doesn't swallow exceptions
let master = '#EXTM3U\n';
options.playlists.forEach((bandwidth) => {
master+= '#EXT-X-STREAM-INF:BANDWIDTH=' + bandwidth + '\n';
master += 'playlist-' + bandwidth + '.m3u8\n';
});
// simulate buffered and currentTime during playback
let getBuffer = (buff) => {
return videojs.createTimeRange(0, currentTime + buffered);
};
player.tech_.buffered = getBuffer;
Object.defineProperty(player.tech_, 'time_', {
get: () => currentTime
});
options.bandwidths.sort(function(left, right) {
return left.time - right.time;
});
// respond to the playlist requests
let masterRequest = requests.shift();
masterRequest.bandwidth = options.bandwidths[0].bandwidth;
masterRequest.respond(200, null, master);
let playlistRequest = requests.shift();
playlistRequest.bandwidth = options.bandwidths[0].bandwidth;
playlistRequest.respond(200, null, playlistResponse(playlistRequest));
let sourceBuffer = player.tech_.hls.mediaSource.sourceBuffers[0];
Object.defineProperty(sourceBuffer, 'buffered', {
get: () => buffered
});
// record the measured bandwidth for the playlist requests
results.effectiveBandwidth.push({
time: 0,
bandwidth: player.tech_.hls.bandwidth
});
// advance time and collect simulation results
for (t = i = 0; t < duration; clock.tick(1000), t++) {
while (options.bandwidths[i + 1] && options.bandwidths[i + 1].time <= t) {
i++;
}
let bandwidth = options.bandwidths[i].bandwidth;
results.bandwidth.push({
time: t,
bandwidth: bandwidth
});
// schedule response deliveries
while (requests.length) {
let request = requests.shift();
request.bandwidth = bandwidth;
// playlist responses
if (/\.m3u8$/.test(request.url)) {
// for simplicity, playlist responses have zero trasmission time
request.respond(200, null, playlistResponse(request));
continue;
}
// segment responses
let segmentSize = request.url.match(/(\d+)-\d+/)[1] * segmentDuration;
//console.log(segmentSize);
//console.log(bandwidth);
console.log(request.url);
let timeToTake = segmentSize/bandwidth + (propagationDelay * 1);
setTimeout(() => {
if (request.aborted) {
console.error("Request for segment aborted, download timedout")
return;
}
request.response = new Uint8Array(segmentSize * 0.125);
request.respond(200, null, '');
sourceBuffer.trigger('updateend');
results.playlists.push({
time: t,
bitrate: +request.url.match(/(\d+)-\d+/)[1]
});
buffered += segmentDuration;
results.effectiveBandwidth.push({
time: t,
bandwidth: player.tech_.hls.bandwidth
});
}, timeToTake * 1000);
// console.log(`taking ${timeToTake}s for response`);
}
results.buffered.push({
time: t,
buffered: buffered
});
// simulate playback
if (buffered > 0) {
buffered--;
currentTime++;
}
player.trigger('timeupdate');
}
// update the fragment identifier so this scenario can be re-run easily
window.location.hash = '#' + options.bandwidths.map(function(interval) {
return 't' + interval.time + '=' + interval.bandwidth;
}).join('&');
player.dispose();
mse.restore();
env.restore();
console.log(results);
done(null, results);
};
export default runSimulation;
import runSimulation from './run-simulation';
import displayTimeline from './display-timeline';
// a dynamic number of time-bandwidth pairs may be defined to drive the simulation
let networkTimeline = document.querySelector('.network-timeline');
let timePeriod = networkTimeline.querySelector('li:last-child').cloneNode(true);
const appendTimePeriod = function() {
let clone = timePeriod.cloneNode(true);
let count = networkTimeline.querySelectorAll('input.bandwidth').length;
let time = clone.querySelector('.time');
let bandwidth = clone.querySelector('input.bandwidth');
time.name = 'time' + count;
bandwidth.name = 'bandwidth' + count;
networkTimeline.appendChild(clone);
};
document.querySelector('.add-time-period').addEventListener('click', appendTimePeriod);
// apply any simulation parameters that were set in the fragment identifier
if (window.location.hash) {
// time periods are specified as t<seconds>=<bitrate>
// e.g. #t15=450560&t150=65530
let params = window.location.hash.substring(1)
.split('&')
.map(function(param) {
return ((/t(\d+)=(\d+)/i).exec(param) || [])
.map(window.parseFloat).slice(1);
}).filter(function(pair) {
return pair.length === 2;
});
networkTimeline.innerHTML = '';
params.forEach(function(param) {
appendTimePeriod();
networkTimeline.querySelector('li:last-child .time').value = param[0];
networkTimeline.querySelector('li:last-child input.bandwidth').value = param[1];
});
}
// collect the simulation parameters
const parameters = function() {
let times = Array.prototype.slice.call(document.querySelectorAll('.time'));
let bandwidths = document.querySelectorAll('input.bandwidth');
let playlists = Array.prototype.slice.call(document.querySelectorAll('input.bitrate'));
return {
playlists: playlists.map(function(input) {
return +input.value;
}),
bandwidths: times.reduce(function(conditions, time, i) {
return conditions.concat({
time: +time.value,
bandwidth: +bandwidths[i].value
});
}, [])
};
};
let runButton = document.getElementById('run-simulation');
runButton.addEventListener('click', function() {
runSimulation(parameters(), displayTimeline);
});
runButton.click();