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.
Showing
2 changed files
with
140 additions
and
42 deletions
... | @@ -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); | ... | ... |
-
Please register or sign in to post a comment