You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
415 lines
10 KiB
415 lines
10 KiB
import document from 'global/document';
|
|
import sinon from 'sinon';
|
|
import window from 'global/window';
|
|
import URLToolkit from 'url-toolkit';
|
|
import videojs from 'video.js';
|
|
/* eslint-disable no-unused-vars */
|
|
// needed so MediaSource can be registered with videojs
|
|
import MediaSource from 'videojs-contrib-media-sources';
|
|
/* eslint-enable */
|
|
import testDataManifests from './test-manifests.js';
|
|
import xhrFactory from '../src/xhr';
|
|
|
|
// a SourceBuffer that tracks updates but otherwise is a noop
|
|
class MockSourceBuffer extends videojs.EventTarget {
|
|
constructor() {
|
|
super();
|
|
this.updates_ = [];
|
|
|
|
this.updating = false;
|
|
this.on('updateend', function() {
|
|
this.updating = false;
|
|
});
|
|
|
|
this.buffered = videojs.createTimeRanges();
|
|
this.duration_ = NaN;
|
|
|
|
Object.defineProperty(this, 'duration', {
|
|
get() {
|
|
return this.duration_;
|
|
},
|
|
set(duration) {
|
|
this.updates_.push({
|
|
duration
|
|
});
|
|
this.duration_ = duration;
|
|
}
|
|
});
|
|
}
|
|
|
|
abort() {
|
|
this.updates_.push({
|
|
abort: true
|
|
});
|
|
}
|
|
|
|
appendBuffer(bytes) {
|
|
this.updates_.push({
|
|
append: bytes
|
|
});
|
|
this.updating = true;
|
|
}
|
|
|
|
remove(start, end) {
|
|
this.updates_.push({
|
|
remove: [start, end]
|
|
});
|
|
}
|
|
}
|
|
|
|
class MockMediaSource extends videojs.EventTarget {
|
|
constructor() {
|
|
super();
|
|
this.readyState = 'closed';
|
|
this.on('sourceopen', function() {
|
|
this.readyState = 'open';
|
|
});
|
|
|
|
this.sourceBuffers = [];
|
|
this.duration = NaN;
|
|
this.seekable = videojs.createTimeRange();
|
|
}
|
|
|
|
addSeekableRange_(start, end) {
|
|
this.seekable = videojs.createTimeRange(start, end);
|
|
}
|
|
|
|
addSourceBuffer(mime) {
|
|
let sourceBuffer = new MockSourceBuffer();
|
|
|
|
sourceBuffer.mimeType_ = mime;
|
|
this.sourceBuffers.push(sourceBuffer);
|
|
return sourceBuffer;
|
|
}
|
|
|
|
endOfStream(error) {
|
|
this.readyState = 'ended';
|
|
this.error_ = error;
|
|
}
|
|
}
|
|
|
|
export class MockTextTrack {
|
|
constructor() {
|
|
this.cues = [];
|
|
}
|
|
addCue(cue) {
|
|
this.cues.push(cue);
|
|
}
|
|
removeCue(cue) {
|
|
for (let i = 0; i < this.cues.length; i++) {
|
|
if (this.cues[i] === cue) {
|
|
this.cues.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// return an absolute version of a page-relative URL
|
|
export const absoluteUrl = function(relativeUrl) {
|
|
return URLToolkit.buildAbsoluteURL(window.location.href, relativeUrl);
|
|
};
|
|
|
|
export const useFakeMediaSource = function() {
|
|
let RealMediaSource = videojs.MediaSource;
|
|
let realCreateObjectURL = videojs.URL.createObjectURL;
|
|
let id = 0;
|
|
|
|
videojs.MediaSource = MockMediaSource;
|
|
videojs.MediaSource.supportsNativeMediaSources =
|
|
RealMediaSource.supportsNativeMediaSources;
|
|
videojs.URL.createObjectURL = function() {
|
|
id++;
|
|
return 'blob:videojs-contrib-hls-mock-url' + id;
|
|
};
|
|
|
|
return {
|
|
restore() {
|
|
videojs.MediaSource = RealMediaSource;
|
|
videojs.URL.createObjectURL = realCreateObjectURL;
|
|
}
|
|
};
|
|
};
|
|
|
|
export const useFakeEnvironment = function(assert) {
|
|
let realXMLHttpRequest = videojs.xhr.XMLHttpRequest;
|
|
|
|
let fakeEnvironment = {
|
|
requests: [],
|
|
restore() {
|
|
this.clock.restore();
|
|
videojs.xhr.XMLHttpRequest = realXMLHttpRequest;
|
|
this.xhr.restore();
|
|
['warn', 'error'].forEach((level) => {
|
|
if (this.log && this.log[level] && this.log[level].restore) {
|
|
if (assert) {
|
|
let calls = (this.log[level].args || []).map((args) => {
|
|
return args.join(', ');
|
|
}).join('\n ');
|
|
|
|
assert.equal(this.log[level].callCount,
|
|
0,
|
|
'no unexpected logs at level "' + level + '":\n ' + calls);
|
|
}
|
|
this.log[level].restore();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
fakeEnvironment.log = {};
|
|
['warn', 'error'].forEach((level) => {
|
|
// you can use .log[level].args to get args
|
|
sinon.stub(videojs.log, level);
|
|
fakeEnvironment.log[level] = videojs.log[level];
|
|
Object.defineProperty(videojs.log[level], 'calls', {
|
|
get() {
|
|
// reset callCount to 0 so they don't have to
|
|
let callCount = this.callCount;
|
|
|
|
this.callCount = 0;
|
|
return callCount;
|
|
}
|
|
});
|
|
});
|
|
fakeEnvironment.clock = sinon.useFakeTimers();
|
|
fakeEnvironment.xhr = sinon.useFakeXMLHttpRequest();
|
|
|
|
// Sinon 1.10.2 handles abort incorrectly (triggering the error event)
|
|
// Later versions fixed this but broke the ability to set the response
|
|
// to an arbitrary object (in our case, a typed array).
|
|
XMLHttpRequest.prototype = Object.create(XMLHttpRequest.prototype);
|
|
XMLHttpRequest.prototype.abort = function abort() {
|
|
this.response = this.responseText = '';
|
|
this.errorFlag = true;
|
|
this.requestHeaders = {};
|
|
this.responseHeaders = {};
|
|
|
|
if (this.readyState > 0 && this.sendFlag) {
|
|
this.readyStateChange(4);
|
|
this.sendFlag = false;
|
|
}
|
|
|
|
this.readyState = 0;
|
|
};
|
|
|
|
XMLHttpRequest.prototype.downloadProgress = function downloadProgress(rawEventData) {
|
|
this.dispatchEvent(new sinon.ProgressEvent('progress',
|
|
rawEventData,
|
|
rawEventData.target));
|
|
};
|
|
|
|
// add support for xhr.responseURL
|
|
XMLHttpRequest.prototype.open = (function(origFn) {
|
|
return function() {
|
|
this.responseURL = absoluteUrl(arguments[1]);
|
|
|
|
return origFn.apply(this, arguments);
|
|
};
|
|
}(XMLHttpRequest.prototype.open));
|
|
|
|
fakeEnvironment.requests.length = 0;
|
|
fakeEnvironment.xhr.onCreate = function(xhr) {
|
|
xhr.responseURL = xhr.url;
|
|
fakeEnvironment.requests.push(xhr);
|
|
};
|
|
videojs.xhr.XMLHttpRequest = fakeEnvironment.xhr;
|
|
|
|
return fakeEnvironment;
|
|
};
|
|
|
|
// patch over some methods of the provided tech so it can be tested
|
|
// synchronously with sinon's fake timers
|
|
export const mockTech = function(tech) {
|
|
if (tech.isMocked_) {
|
|
// make this function idempotent because HTML and Flash based
|
|
// playback have very different lifecycles. For HTML, the tech
|
|
// is available on player creation. For Flash, the tech isn't
|
|
// ready until the source has been loaded and one tick has
|
|
// expired.
|
|
return;
|
|
}
|
|
|
|
tech.isMocked_ = true;
|
|
tech.src_ = null;
|
|
tech.time_ = null;
|
|
|
|
tech.paused_ = !tech.autoplay();
|
|
tech.paused = function() {
|
|
return tech.paused_;
|
|
};
|
|
|
|
if (!tech.currentTime_) {
|
|
tech.currentTime_ = tech.currentTime;
|
|
}
|
|
tech.currentTime = function() {
|
|
return tech.time_ === null ? tech.currentTime_() : tech.time_;
|
|
};
|
|
|
|
tech.setSrc = function(src) {
|
|
tech.src_ = src;
|
|
};
|
|
tech.src = function(src) {
|
|
if (src !== null) {
|
|
return tech.setSrc(src);
|
|
}
|
|
return tech.src_ === null ? tech.src : tech.src_;
|
|
};
|
|
tech.currentSrc_ = tech.currentSrc;
|
|
tech.currentSrc = function() {
|
|
return tech.src_ === null ? tech.currentSrc_() : tech.src_;
|
|
};
|
|
|
|
tech.play_ = tech.play;
|
|
tech.play = function() {
|
|
tech.play_();
|
|
tech.paused_ = false;
|
|
tech.trigger('play');
|
|
};
|
|
tech.pause_ = tech.pause;
|
|
tech.pause = function() {
|
|
tech.pause_();
|
|
tech.paused_ = true;
|
|
tech.trigger('pause');
|
|
};
|
|
|
|
tech.setCurrentTime = function(time) {
|
|
tech.time_ = time;
|
|
|
|
setTimeout(function() {
|
|
tech.trigger('seeking');
|
|
setTimeout(function() {
|
|
tech.trigger('seeked');
|
|
}, 1);
|
|
}, 1);
|
|
};
|
|
};
|
|
|
|
export const createPlayer = function(options, src, clock) {
|
|
let video;
|
|
let player;
|
|
|
|
video = document.createElement('video');
|
|
video.className = 'video-js';
|
|
if (src) {
|
|
if (typeof src === 'string') {
|
|
video.src = src;
|
|
} else if (src.src) {
|
|
let source = document.createElement('source');
|
|
|
|
source.src = src.src;
|
|
if (src.type) {
|
|
source.type = src.type;
|
|
}
|
|
video.appendChild(source);
|
|
}
|
|
}
|
|
document.querySelector('#qunit-fixture').appendChild(video);
|
|
player = videojs(video, options || {
|
|
flash: {
|
|
swf: ''
|
|
}
|
|
});
|
|
|
|
player.buffered = function() {
|
|
return videojs.createTimeRange(0, 0);
|
|
};
|
|
|
|
if (clock) {
|
|
clock.tick(1);
|
|
}
|
|
|
|
mockTech(player.tech_);
|
|
|
|
return player;
|
|
};
|
|
|
|
export const openMediaSource = function(player, clock) {
|
|
// ensure the Flash tech is ready
|
|
player.tech_.triggerReady();
|
|
clock.tick(1);
|
|
// mock the tech *after* it has finished loading so that we don't
|
|
// mock a tech that will be unloaded on the next tick
|
|
mockTech(player.tech_);
|
|
player.tech_.hls.xhr = xhrFactory();
|
|
|
|
// simulate the sourceopen event
|
|
player.tech_.hls.mediaSource.readyState = 'open';
|
|
player.tech_.hls.mediaSource.dispatchEvent({
|
|
type: 'sourceopen',
|
|
swfId: player.tech_.el().id
|
|
});
|
|
clock.tick(1);
|
|
};
|
|
|
|
export const standardXHRResponse = function(request, data) {
|
|
if (!request.url) {
|
|
return;
|
|
}
|
|
|
|
let contentType = 'application/json';
|
|
// contents off the global object
|
|
let manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
|
|
|
|
if (manifestName) {
|
|
manifestName = manifestName[1];
|
|
} else {
|
|
manifestName = request.url;
|
|
}
|
|
|
|
if (/\.m3u8?/.test(request.url)) {
|
|
contentType = 'application/vnd.apple.mpegurl';
|
|
} else if (/\.ts/.test(request.url)) {
|
|
contentType = 'video/MP2T';
|
|
}
|
|
|
|
if (!data) {
|
|
data = testDataManifests[manifestName];
|
|
}
|
|
|
|
request.response = new Uint8Array(1024).buffer;
|
|
request.respond(200, {'Content-Type': contentType}, data);
|
|
};
|
|
|
|
export const playlistWithDuration = function(time, conf) {
|
|
let result = {
|
|
targetDuration: 10,
|
|
mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0,
|
|
discontinuityStarts: [],
|
|
segments: [],
|
|
endList: conf && typeof conf.endList !== 'undefined' ? !!conf.endList : true,
|
|
uri: conf && typeof conf.uri !== 'undefined' ? conf.uri : 'playlist.m3u8',
|
|
discontinuitySequence:
|
|
conf && conf.discontinuitySequence ? conf.discontinuitySequence : 0,
|
|
attributes: conf && typeof conf.attributes !== 'undefined' ? conf.attributes : {}
|
|
};
|
|
let count = Math.floor(time / 10);
|
|
let remainder = time % 10;
|
|
let i;
|
|
let isEncrypted = conf && conf.isEncrypted;
|
|
let extension = conf && conf.extension ? conf.extension : '.ts';
|
|
|
|
for (i = 0; i < count; i++) {
|
|
result.segments.push({
|
|
uri: i + extension,
|
|
resolvedUri: i + extension,
|
|
duration: 10,
|
|
timeline: result.discontinuitySequence
|
|
});
|
|
if (isEncrypted) {
|
|
result.segments[i].key = {
|
|
uri: i + '-key.php',
|
|
resolvedUri: i + '-key.php'
|
|
};
|
|
}
|
|
}
|
|
if (remainder) {
|
|
result.segments.push({
|
|
uri: i + extension,
|
|
duration: remainder,
|
|
timeline: result.discontinuitySequence
|
|
});
|
|
}
|
|
return result;
|
|
};
|