UNPKG

10.4 kBJavaScriptView Raw
1import document from 'global/document';
2import sinon from 'sinon';
3import videojs from 'video.js';
4/* eslint-disable no-unused-vars */
5// needed so MediaSource can be registered with videojs
6import MediaSource from 'videojs-contrib-media-sources';
7/* eslint-enable */
8import testDataManifests from './test-manifests.js';
9import xhrFactory from '../src/xhr';
10import window from 'global/window';
11
12// a SourceBuffer that tracks updates but otherwise is a noop
13class MockSourceBuffer extends videojs.EventTarget {
14 constructor() {
15 super();
16 this.updates_ = [];
17
18 this.updating = false;
19 this.on('updateend', function() {
20 this.updating = false;
21 });
22
23 this.buffered = videojs.createTimeRanges();
24 this.duration_ = NaN;
25
26 Object.defineProperty(this, 'duration', {
27 get() {
28 return this.duration_;
29 },
30 set(duration) {
31 this.updates_.push({
32 duration
33 });
34 this.duration_ = duration;
35 }
36 });
37 }
38
39 abort() {
40 this.updates_.push({
41 abort: true
42 });
43 }
44
45 appendBuffer(bytes) {
46 this.updates_.push({
47 append: bytes
48 });
49 this.updating = true;
50 }
51
52 remove(start, end) {
53 this.updates_.push({
54 remove: [start, end]
55 });
56 }
57}
58
59class MockMediaSource extends videojs.EventTarget {
60 constructor() {
61 super();
62 this.readyState = 'closed';
63 this.on('sourceopen', function() {
64 this.readyState = 'open';
65 });
66
67 this.sourceBuffers = [];
68 this.duration = NaN;
69 this.seekable = videojs.createTimeRange();
70 }
71
72 addSeekableRange_(start, end) {
73 this.seekable = videojs.createTimeRange(start, end);
74 }
75
76 addSourceBuffer(mime) {
77 let sourceBuffer = new MockSourceBuffer();
78
79 sourceBuffer.mimeType_ = mime;
80 this.sourceBuffers.push(sourceBuffer);
81 return sourceBuffer;
82 }
83
84 endOfStream(error) {
85 this.readyState = 'ended';
86 this.error_ = error;
87 }
88}
89
90export class MockTextTrack {
91 constructor() {
92 this.cues = [];
93 }
94 addCue(cue) {
95 this.cues.push(cue);
96 }
97 removeCue(cue) {
98 for (let i = 0; i < this.cues.length; i++) {
99 if (this.cues[i] === cue) {
100 this.cues.splice(i, 1);
101 break;
102 }
103 }
104 }
105}
106
107export const useFakeMediaSource = function() {
108 let RealMediaSource = videojs.MediaSource;
109 let realCreateObjectURL = videojs.URL.createObjectURL;
110 let id = 0;
111
112 videojs.MediaSource = MockMediaSource;
113 videojs.MediaSource.supportsNativeMediaSources =
114 RealMediaSource.supportsNativeMediaSources;
115 videojs.URL.createObjectURL = function() {
116 id++;
117 return 'blob:videojs-contrib-hls-mock-url' + id;
118 };
119
120 return {
121 restore() {
122 videojs.MediaSource = RealMediaSource;
123 videojs.URL.createObjectURL = realCreateObjectURL;
124 }
125 };
126};
127
128export const useFakeEnvironment = function(assert) {
129 let realXMLHttpRequest = videojs.xhr.XMLHttpRequest;
130
131 let fakeEnvironment = {
132 requests: [],
133 restore() {
134 this.clock.restore();
135 videojs.xhr.XMLHttpRequest = realXMLHttpRequest;
136 this.xhr.restore();
137 ['warn', 'error'].forEach((level) => {
138 if (this.log && this.log[level] && this.log[level].restore) {
139 if (assert) {
140 let calls = (this.log[level].args || []).map((args) => {
141 return args.join(', ');
142 }).join('\n ');
143
144 assert.equal(this.log[level].callCount,
145 0,
146 'no unexpected logs at level "' + level + '":\n ' + calls);
147 }
148 this.log[level].restore();
149 }
150 });
151 }
152 };
153
154 fakeEnvironment.log = {};
155 ['warn', 'error'].forEach((level) => {
156 // you can use .log[level].args to get args
157 sinon.stub(videojs.log, level);
158 fakeEnvironment.log[level] = videojs.log[level];
159 Object.defineProperty(videojs.log[level], 'calls', {
160 get() {
161 // reset callCount to 0 so they don't have to
162 let callCount = this.callCount;
163
164 this.callCount = 0;
165 return callCount;
166 }
167 });
168 });
169 fakeEnvironment.clock = sinon.useFakeTimers();
170 fakeEnvironment.xhr = sinon.useFakeXMLHttpRequest();
171
172 // Sinon 1.10.2 handles abort incorrectly (triggering the error event)
173 // Later versions fixed this but broke the ability to set the response
174 // to an arbitrary object (in our case, a typed array).
175 XMLHttpRequest.prototype = Object.create(XMLHttpRequest.prototype);
176 XMLHttpRequest.prototype.abort = function abort() {
177 this.response = this.responseText = '';
178 this.errorFlag = true;
179 this.requestHeaders = {};
180 this.responseHeaders = {};
181
182 if (this.readyState > 0 && this.sendFlag) {
183 this.readyStateChange(4);
184 this.sendFlag = false;
185 }
186
187 this.readyState = 0;
188 };
189
190 XMLHttpRequest.prototype.downloadProgress = function downloadProgress(rawEventData) {
191 this.dispatchEvent(new sinon.ProgressEvent('progress',
192 rawEventData,
193 rawEventData.target));
194 };
195
196 fakeEnvironment.requests.length = 0;
197 fakeEnvironment.xhr.onCreate = function(xhr) {
198 fakeEnvironment.requests.push(xhr);
199 };
200 videojs.xhr.XMLHttpRequest = fakeEnvironment.xhr;
201
202 return fakeEnvironment;
203};
204
205// patch over some methods of the provided tech so it can be tested
206// synchronously with sinon's fake timers
207export const mockTech = function(tech) {
208 if (tech.isMocked_) {
209 // make this function idempotent because HTML and Flash based
210 // playback have very different lifecycles. For HTML, the tech
211 // is available on player creation. For Flash, the tech isn't
212 // ready until the source has been loaded and one tick has
213 // expired.
214 return;
215 }
216
217 tech.isMocked_ = true;
218 tech.src_ = null;
219 tech.time_ = null;
220
221 tech.paused_ = !tech.autoplay();
222 tech.paused = function() {
223 return tech.paused_;
224 };
225
226 if (!tech.currentTime_) {
227 tech.currentTime_ = tech.currentTime;
228 }
229 tech.currentTime = function() {
230 return tech.time_ === null ? tech.currentTime_() : tech.time_;
231 };
232
233 tech.setSrc = function(src) {
234 tech.src_ = src;
235 };
236 tech.src = function(src) {
237 if (src !== null) {
238 return tech.setSrc(src);
239 }
240 return tech.src_ === null ? tech.src : tech.src_;
241 };
242 tech.currentSrc_ = tech.currentSrc;
243 tech.currentSrc = function() {
244 return tech.src_ === null ? tech.currentSrc_() : tech.src_;
245 };
246
247 tech.play_ = tech.play;
248 tech.play = function() {
249 tech.play_();
250 tech.paused_ = false;
251 tech.trigger('play');
252 };
253 tech.pause_ = tech.pause;
254 tech.pause = function() {
255 tech.pause_();
256 tech.paused_ = true;
257 tech.trigger('pause');
258 };
259
260 tech.setCurrentTime = function(time) {
261 tech.time_ = time;
262
263 setTimeout(function() {
264 tech.trigger('seeking');
265 setTimeout(function() {
266 tech.trigger('seeked');
267 }, 1);
268 }, 1);
269 };
270};
271
272export const createPlayer = function(options, src, clock) {
273 let video;
274 let player;
275
276 video = document.createElement('video');
277 video.className = 'video-js';
278 if (src) {
279 if (typeof src === 'string') {
280 video.src = src;
281 } else if (src.src) {
282 let source = document.createElement('source');
283
284 source.src = src.src;
285 if (src.type) {
286 source.type = src.type;
287 }
288 video.appendChild(source);
289 }
290 }
291 document.querySelector('#qunit-fixture').appendChild(video);
292 player = videojs(video, options || {
293 flash: {
294 swf: ''
295 }
296 });
297
298 player.buffered = function() {
299 return videojs.createTimeRange(0, 0);
300 };
301
302 if (clock) {
303 clock.tick(1);
304 }
305
306 mockTech(player.tech_);
307
308 return player;
309};
310
311export const openMediaSource = function(player, clock) {
312 // ensure the Flash tech is ready
313 player.tech_.triggerReady();
314 clock.tick(1);
315 // mock the tech *after* it has finished loading so that we don't
316 // mock a tech that will be unloaded on the next tick
317 mockTech(player.tech_);
318 player.tech_.hls.xhr = xhrFactory();
319
320 // simulate the sourceopen event
321 player.tech_.hls.mediaSource.readyState = 'open';
322 player.tech_.hls.mediaSource.dispatchEvent({
323 type: 'sourceopen',
324 swfId: player.tech_.el().id
325 });
326 clock.tick(1);
327};
328
329export const standardXHRResponse = function(request, data) {
330 if (!request.url) {
331 return;
332 }
333
334 let contentType = 'application/json';
335 // contents off the global object
336 let manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
337
338 if (manifestName) {
339 manifestName = manifestName[1];
340 } else {
341 manifestName = request.url;
342 }
343
344 if (/\.m3u8?/.test(request.url)) {
345 contentType = 'application/vnd.apple.mpegurl';
346 } else if (/\.ts/.test(request.url)) {
347 contentType = 'video/MP2T';
348 }
349
350 if (!data) {
351 data = testDataManifests[manifestName];
352 }
353
354 request.response = new Uint8Array(1024).buffer;
355 request.respond(200, {'Content-Type': contentType}, data);
356};
357
358// return an absolute version of a page-relative URL
359export const absoluteUrl = function(relativeUrl) {
360 return window.location.protocol + '//' +
361 window.location.host +
362 (window.location.pathname
363 .split('/')
364 .slice(0, -1)
365 .concat(relativeUrl)
366 .join('/')
367 );
368};
369
370export const playlistWithDuration = function(time, conf) {
371 let result = {
372 targetDuration: 10,
373 mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0,
374 discontinuityStarts: [],
375 segments: [],
376 endList: conf && typeof conf.endList !== 'undefined' ? !!conf.endList : true,
377 uri: conf && typeof conf.uri !== 'undefined' ? conf.uri : 'playlist.m3u8',
378 discontinuitySequence:
379 conf && conf.discontinuitySequence ? conf.discontinuitySequence : 0,
380 attributes: {}
381 };
382 let count = Math.floor(time / 10);
383 let remainder = time % 10;
384 let i;
385 let isEncrypted = conf && conf.isEncrypted;
386 let extension = conf && conf.extension ? conf.extension : '.ts';
387
388 for (i = 0; i < count; i++) {
389 result.segments.push({
390 uri: i + extension,
391 resolvedUri: i + extension,
392 duration: 10,
393 timeline: result.discontinuitySequence
394 });
395 if (isEncrypted) {
396 result.segments[i].key = {
397 uri: i + '-key.php',
398 resolvedUri: i + '-key.php'
399 };
400 }
401 }
402 if (remainder) {
403 result.segments.push({
404 uri: i + extension,
405 duration: remainder,
406 timeline: result.discontinuitySequence
407 });
408 }
409 return result;
410};