04fd771a by David LaPalomento

Simulate time and XHR responses

Make the simulation run asynchronous so flash doesn't swallow exceptions in the ExternalInterface callback. Hook up mechanisms to advance the clock and simulate XHR responses for playlists and segments. Currently, segment responses are delivered as if the bandwidth at the current time was available for the entire transmission time, which is not correct.
1 parent 0fe4d140
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
57 After <input name=time0 class=time type=number value=0 min=0> seconds, 57 After <input name=time0 class=time type=number value=0 min=0> seconds,
58 </label> 58 </label>
59 <label> 59 <label>
60 the link capacity is <input name=bandwidth0 class=bandwidth type=number value=100> bits per second 60 the link capacity is <input name=bandwidth0 class=bandwidth type=number value=32768> bits per second
61 </label> 61 </label>
62 </li> 62 </li>
63 </ol> 63 </ol>
......
1 (function(window, document) { 1 (function(window, document) {
2 'use strict'; 2 'use strict';
3 var segmentDuration = 9, // seconds 3 var segmentDuration = 9, // seconds
4 duration = segmentDuration * 100, // 100 segments 4 segmentCount = 100,
5 5 duration = segmentDuration * segmentCount,
6 clock, 6 propagationDelay = 1,
7 fakeXhr,
8 requests,
9 7
10 runSimulation, 8 runSimulation,
9 playlistResponse,
11 player, 10 player,
12 runButton, 11 runButton,
13 parameters, 12 parameters,
...@@ -19,17 +18,16 @@ ...@@ -19,17 +18,16 @@
19 displayTimeline; 18 displayTimeline;
20 19
21 // mock out the environment and dependencies 20 // mock out the environment and dependencies
22 clock = sinon.useFakeTimers();
23 fakeXhr = sinon.useFakeXMLHttpRequest();
24 requests = [];
25 fakeXhr.onCreate = function(xhr) {
26 requests.push(xhr);
27 };
28 videojs.options.flash.swf = '../../node_modules/video.js/dist/video-js/video-js.swf'; 21 videojs.options.flash.swf = '../../node_modules/video.js/dist/video-js/video-js.swf';
29 videojs.Hls.SegmentParser = function() { 22 videojs.Hls.SegmentParser = function() {
30 this.getFlvHeader = function() { 23 this.getFlvHeader = function() {
31 return new Uint8Array([]); 24 return new Uint8Array([]);
32 } 25 };
26 this.parseSegmentBinaryData = function() {};
27 this.flushTags = function() {};
28 this.tagsAvailable = function() {
29 return false;
30 };
33 }; 31 };
34 32
35 // a dynamic number of time-bandwidth pairs may be defined to drive the simulation 33 // a dynamic number of time-bandwidth pairs may be defined to drive the simulation
...@@ -70,20 +68,49 @@ ...@@ -70,20 +68,49 @@
70 }; 68 };
71 }; 69 };
72 70
71 // send a mock playlist response
72 playlistResponse = function(bitrate) {
73 var i = segmentCount,
74 response = '#EXTM3U\n';
75
76 while (i--) {
77 response += '#EXTINF:' + segmentDuration + ',\n';
78 response += bitrate + '-' + (segmentCount - i) + '\n';
79 }
80 response += '#EXT-X-ENDLIST\n';
81
82 return response;
83 };
84
73 // run the simulation 85 // run the simulation
74 runSimulation = function(options) { 86 runSimulation = function(options, done) {
75 var results = [], 87 var results = [],
76 bandwidths = options.bandwidths, 88 bandwidths = options.bandwidths,
77 fixture = document.getElementById('fixture'), 89 fixture = document.getElementById('fixture'),
90
91 realSetTimeout = window.setTimeout,
92 clock,
93 fakeXhr,
94 requests,
78 video, 95 video,
79 t, 96 t = 0,
80 i; 97 i = 0;
81 98
82 // clean up the last run if necessary 99 // clean up the last run if necessary
83 if (player) { 100 if (player) {
84 player.dispose(); 101 player.dispose();
85 }; 102 };
86 103
104
105 // mock out the environment
106 clock = sinon.useFakeTimers();
107 fakeXhr = sinon.useFakeXMLHttpRequest();
108 requests = [];
109 fakeXhr.onCreate = function(xhr) {
110 xhr.startTime = t;
111 requests.push(xhr);
112 };
113
87 // initialize the HLS tech 114 // initialize the HLS tech
88 fixture.innerHTML = ''; 115 fixture.innerHTML = '';
89 video = document.createElement('video'); 116 video = document.createElement('video');
...@@ -95,35 +122,107 @@ ...@@ -95,35 +122,107 @@
95 type: 'application/x-mpegurl' 122 type: 'application/x-mpegurl'
96 }] 123 }]
97 }); 124 });
125
98 player.ready(function() { 126 player.ready(function() {
99 var master = '#EXTM3U\n' + 127 // run next tick so that Flash doesn't swallow exceptions
100 options.playlists.reduce(function(playlists, value) { 128 realSetTimeout(function() {
101 return playlists + 129 var master = '#EXTM3U\n' +
102 '#EXT-X-STREAM-INF:' + value + '\n' + 130 options.playlists.reduce(function(playlists, value) {
103 value + '\n'; 131 return playlists +
104 }, ''); 132 '#EXT-X-STREAM-INF:' + value + '\n' +
105 requests.pop().respond(200, null, master); 133 'playlist-' + value + '\n';
106 }); 134 }, ''),
135 buffered = 0,
136 currentTime = 0;
107 137
108 // bandwidth 138 // mock out buffered and currentTime
109 bandwidths.sort(function(left, right) { 139 player.buffered = function() {
110 return left.time - right.time; 140 return videojs.createTimeRange(0, currentTime + buffered);
111 }); 141 };
142 player.currentTime = function() {
143 return currentTime;
144 };
112 145
113 for (t = i = 0; t < duration; t++) { 146 // respond to the playlist requests
114 while (bandwidths[i + 1] && bandwidths[i + 1].time <= t) { 147 requests.shift().respond(200, null, master);
115 i++; 148 requests[0].respond(200, null, playlistResponse(+requests[0].url.match(/\d+$/)));
116 } 149 requests.shift();
117 results.push({ 150
118 time: t, 151 bandwidths.sort(function(left, right) {
119 bandwidth: bandwidths[i].bandwidth 152 return left.time - right.time;
120 }); 153 });
121 } 154
122 return results; 155 // advance time and collect simulation results
156 for (t = i = 0; t < duration; clock.tick(1 * 1000), t++) {
157
158 // determine the bandwidth value at this moment
159 while (bandwidths[i + 1] && bandwidths[i + 1].time <= t) {
160 i++;
161 }
162 results.push({
163 time: t,
164 bandwidth: bandwidths[i].bandwidth
165 });
166
167 // deliver responses if they're ready
168 requests = requests.reduce(function(remaining, request) {
169 var arrival = request.startTime + propagationDelay,
170 delivered = Math.max(0, bandwidths[i].bandwidth * (t - arrival)),
171 playlist = /playlist-\d+$/.test(request.url),
172 segmentSize = +request.url.match(/(\d+)-\d+$/)[1] * segmentDuration;
173
174 // playlist responses
175 if (playlist) {
176 // playlist responses have no trasmission time
177 if (t === arrival) {
178 request.respond(200, null, playlistResponse(+requests[0].url.match(/\d+$/)));
179 return remaining;
180 }
181 // the response has not arrived. re-enqueue it for later.
182 return remaining.concat(request);
183 }
184
185 // segment responses
186 if (delivered > segmentSize) {
187 // segment responses are delivered after the propagation
188 // delay and the transmission time have elapsed
189 buffered += segmentDuration;
190 request.status = 200;
191 request.response = new Uint8Array(segmentSize);
192 request.setResponseBody('');
193 return remaining;
194 } else {
195 if (t === arrival) {
196 // segment response headers arrive after the propogation delay
197 request.setResponseHeaders({
198 'Content-Type': 'video/mp2t'
199 });
200 }
201 // the response has not arrived fully. re-enqueue it for
202 // later
203 return remaining.concat(request);
204 }
205 }, []);
206
207 // simulate playback
208 if (buffered > 0) {
209 buffered--;
210 currentTime++;
211 }
212 player.trigger('timeupdate');
213 }
214
215 // restore the environment
216 clock.restore();
217 fakeXhr.restore();
218
219 done(null, results);
220 }, 0);
221 });
123 }; 222 };
124 runButton = document.getElementById('run-simulation'); 223 runButton = document.getElementById('run-simulation');
125 runButton.addEventListener('click', function() { 224 runButton.addEventListener('click', function() {
126 displayTimeline(runSimulation(parameters())); 225 runSimulation(parameters(), displayTimeline);
127 }); 226 });
128 227
129 // render the timeline with d3 228 // render the timeline with d3
...@@ -145,7 +244,7 @@ ...@@ -145,7 +244,7 @@
145 .append('g') 244 .append('g')
146 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 245 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
147 246
148 displayTimeline = function(bandwidth) { 247 displayTimeline = function(error, bandwidth) {
149 var x = d3.scale.linear().range([0, width]), 248 var x = d3.scale.linear().range([0, width]),
150 y = d3.scale.linear().range([height, 0]), 249 y = d3.scale.linear().range([height, 0]),
151 250
...@@ -193,7 +292,6 @@ ...@@ -193,7 +292,6 @@
193 }; 292 };
194 })(); 293 })();
195 294
196 295 runSimulation(parameters(), displayTimeline);
197 displayTimeline(runSimulation(parameters()));
198 296
199 })(window, document); 297 })(window, document);
......