1 | import document from 'global/document';
|
2 | import sinon from 'sinon';
|
3 | import videojs from 'video.js';
|
4 |
|
5 |
|
6 | import MediaSource from 'videojs-contrib-media-sources';
|
7 |
|
8 | import testDataManifests from './test-manifests.js';
|
9 | import xhrFactory from '../src/xhr';
|
10 | import window from 'global/window';
|
11 |
|
12 |
|
13 | class 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 |
|
59 | class 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 |
|
90 | export 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 |
|
107 | export 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 |
|
128 | export 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 |
|
157 | sinon.stub(videojs.log, level);
|
158 | fakeEnvironment.log[level] = videojs.log[level];
|
159 | Object.defineProperty(videojs.log[level], 'calls', {
|
160 | get() {
|
161 |
|
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 |
|
173 |
|
174 |
|
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 |
|
206 |
|
207 | export const mockTech = function(tech) {
|
208 | if (tech.isMocked_) {
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
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 |
|
272 | export 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 |
|
311 | export const openMediaSource = function(player, clock) {
|
312 |
|
313 | player.tech_.triggerReady();
|
314 | clock.tick(1);
|
315 |
|
316 |
|
317 | mockTech(player.tech_);
|
318 | player.tech_.hls.xhr = xhrFactory();
|
319 |
|
320 |
|
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 |
|
329 | export const standardXHRResponse = function(request, data) {
|
330 | if (!request.url) {
|
331 | return;
|
332 | }
|
333 |
|
334 | let contentType = 'application/json';
|
335 |
|
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 |
|
359 | export 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 |
|
370 | export 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 | };
|