UNPKG

10.1 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 fakeEnvironment.requests.length = 0;
191 fakeEnvironment.xhr.onCreate = function(xhr) {
192 fakeEnvironment.requests.push(xhr);
193 };
194 videojs.xhr.XMLHttpRequest = fakeEnvironment.xhr;
195
196 return fakeEnvironment;
197};
198
199// patch over some methods of the provided tech so it can be tested
200// synchronously with sinon's fake timers
201export const mockTech = function(tech) {
202 if (tech.isMocked_) {
203 // make this function idempotent because HTML and Flash based
204 // playback have very different lifecycles. For HTML, the tech
205 // is available on player creation. For Flash, the tech isn't
206 // ready until the source has been loaded and one tick has
207 // expired.
208 return;
209 }
210
211 tech.isMocked_ = true;
212 tech.src_ = null;
213 tech.time_ = null;
214
215 tech.paused_ = !tech.autoplay();
216 tech.paused = function() {
217 return tech.paused_;
218 };
219
220 if (!tech.currentTime_) {
221 tech.currentTime_ = tech.currentTime;
222 }
223 tech.currentTime = function() {
224 return tech.time_ === null ? tech.currentTime_() : tech.time_;
225 };
226
227 tech.setSrc = function(src) {
228 tech.src_ = src;
229 };
230 tech.src = function(src) {
231 if (src !== null) {
232 return tech.setSrc(src);
233 }
234 return tech.src_ === null ? tech.src : tech.src_;
235 };
236 tech.currentSrc_ = tech.currentSrc;
237 tech.currentSrc = function() {
238 return tech.src_ === null ? tech.currentSrc_() : tech.src_;
239 };
240
241 tech.play_ = tech.play;
242 tech.play = function() {
243 tech.play_();
244 tech.paused_ = false;
245 tech.trigger('play');
246 };
247 tech.pause_ = tech.pause;
248 tech.pause = function() {
249 tech.pause_();
250 tech.paused_ = true;
251 tech.trigger('pause');
252 };
253
254 tech.setCurrentTime = function(time) {
255 tech.time_ = time;
256
257 setTimeout(function() {
258 tech.trigger('seeking');
259 setTimeout(function() {
260 tech.trigger('seeked');
261 }, 1);
262 }, 1);
263 };
264};
265
266export const createPlayer = function(options, src, clock) {
267 let video;
268 let player;
269
270 video = document.createElement('video');
271 video.className = 'video-js';
272 if (src) {
273 if (typeof src === 'string') {
274 video.src = src;
275 } else if (src.src) {
276 let source = document.createElement('source');
277
278 source.src = src.src;
279 if (src.type) {
280 source.type = src.type;
281 }
282 video.appendChild(source);
283 }
284 }
285 document.querySelector('#qunit-fixture').appendChild(video);
286 player = videojs(video, options || {
287 flash: {
288 swf: ''
289 }
290 });
291
292 player.buffered = function() {
293 return videojs.createTimeRange(0, 0);
294 };
295
296 if (clock) {
297 clock.tick(1);
298 }
299
300 mockTech(player.tech_);
301
302 return player;
303};
304
305export const openMediaSource = function(player, clock) {
306 // ensure the Flash tech is ready
307 player.tech_.triggerReady();
308 clock.tick(1);
309 // mock the tech *after* it has finished loading so that we don't
310 // mock a tech that will be unloaded on the next tick
311 mockTech(player.tech_);
312 player.tech_.hls.xhr = xhrFactory();
313
314 // simulate the sourceopen event
315 player.tech_.hls.mediaSource.readyState = 'open';
316 player.tech_.hls.mediaSource.dispatchEvent({
317 type: 'sourceopen',
318 swfId: player.tech_.el().id
319 });
320 clock.tick(1);
321};
322
323export const standardXHRResponse = function(request, data) {
324 if (!request.url) {
325 return;
326 }
327
328 let contentType = 'application/json';
329 // contents off the global object
330 let manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
331
332 if (manifestName) {
333 manifestName = manifestName[1];
334 } else {
335 manifestName = request.url;
336 }
337
338 if (/\.m3u8?/.test(request.url)) {
339 contentType = 'application/vnd.apple.mpegurl';
340 } else if (/\.ts/.test(request.url)) {
341 contentType = 'video/MP2T';
342 }
343
344 if (!data) {
345 data = testDataManifests[manifestName];
346 }
347
348 request.response = new Uint8Array(1024).buffer;
349 request.respond(200, {'Content-Type': contentType}, data);
350};
351
352// return an absolute version of a page-relative URL
353export const absoluteUrl = function(relativeUrl) {
354 return window.location.protocol + '//' +
355 window.location.host +
356 (window.location.pathname
357 .split('/')
358 .slice(0, -1)
359 .concat(relativeUrl)
360 .join('/')
361 );
362};
363
364export const playlistWithDuration = function(time, conf) {
365 let result = {
366 targetDuration: 10,
367 mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0,
368 discontinuityStarts: [],
369 segments: [],
370 endList: conf && typeof conf.endList !== 'undefined' ? !!conf.endList : true,
371 uri: conf && typeof conf.uri !== 'undefined' ? conf.uri : 'playlist.m3u8',
372 discontinuitySequence:
373 conf && conf.discontinuitySequence ? conf.discontinuitySequence : 0,
374 attributes: {}
375 };
376 let count = Math.floor(time / 10);
377 let remainder = time % 10;
378 let i;
379 let isEncrypted = conf && conf.isEncrypted;
380 let extension = conf && conf.extension ? conf.extension : '.ts';
381
382 for (i = 0; i < count; i++) {
383 result.segments.push({
384 uri: i + extension,
385 resolvedUri: i + extension,
386 duration: 10,
387 timeline: result.discontinuitySequence
388 });
389 if (isEncrypted) {
390 result.segments[i].key = {
391 uri: i + '-key.php',
392 resolvedUri: i + '-key.php'
393 };
394 }
395 }
396 if (remainder) {
397 result.segments.push({
398 uri: i + extension,
399 duration: remainder,
400 timeline: result.discontinuitySequence
401 });
402 }
403 return result;
404};