videojs-contrib-dash
Version:
A Video.js source-handler providing MPEG-DASH playback.
491 lines (414 loc) • 15.6 kB
JavaScript
import videojs from 'video.js';
import QUnit from 'qunit';
import window from 'global/window';
import document from 'global/document';
const dashjs = window.dashjs;
let sampleSrc = {
src: 'movie.mpd',
type: 'application/dash+xml',
keySystemOptions: [{
name: 'com.widevine.alpha',
options: {
extra: 'data',
licenseUrl: 'https://example.com/license'
}
}]
};
const sampleSrcNoDRM = {
src: 'movie.mpd',
type: 'application/dash+xml'
};
const testHandleSource = function(assert, source, expectedKeySystemOptions, config) {
if (config === undefined) {
config = {};
}
const eventHandlers = config.eventHandlers ? config.eventHandlers : {};
let startupCalled = false;
let attachViewCalled = false;
let setLimitBitrateByPortalCalled = false;
let setLimitBitrateByPortalValue = null;
let setTextDefaultEnabledCalled = false;
let setTextDefaultEnabledValue = null;
const el = document.createElement('div');
const fixture = document.querySelector('#qunit-fixture');
// stubs
const origMediaPlayer = dashjs.MediaPlayer;
const origVJSXHR = videojs.xhr;
assert.expect(9);
// Default limitBitrateByPortal to false
const limitBitrateByPortal = config.limitBitrateByPortal || false;
el.setAttribute('id', 'test-vid');
fixture.appendChild(el);
const Html5 = videojs.getTech('Html5');
const tech = new Html5({
playerId: el.getAttribute('id')
});
const options = {
playerId: el.getAttribute('id'),
dash: {
limitBitrateByPortal
}
};
tech.el = function() {
return el;
};
tech.triggerReady = function() { };
dashjs.MediaPlayer = function() {
return {
create() {
return {
initialize() {
startupCalled = true;
},
attachView() {
attachViewCalled = true;
},
setAutoPlay(autoplay) {
assert.strictEqual(autoplay, false, 'autoplay is set to false by default');
},
setProtectionData(keySystemOptions) {
assert.deepEqual(keySystemOptions, expectedKeySystemOptions,
'src and manifest key system options are merged');
},
attachSource(manifest) {
assert.deepEqual(manifest, source.src, 'manifest url is sent to attachSource');
assert.strictEqual(setLimitBitrateByPortalCalled, true,
'MediaPlayer.setLimitBitrateByPortal was called');
assert.strictEqual(setLimitBitrateByPortalValue, limitBitrateByPortal,
'MediaPlayer.setLimitBitrateByPortal was called with the correct value');
assert.strictEqual(setTextDefaultEnabledCalled, true,
'MediaPlayer.setTextDefaultEnabled was called');
assert.strictEqual(setTextDefaultEnabledValue, false,
'MediaPlayer.setTextDefaultEnabled was called with the correct value');
assert.strictEqual(startupCalled, true, 'MediaPlayer.startup was called');
assert.strictEqual(attachViewCalled, true, 'MediaPlayer.attachView was called');
tech.dispose();
// Restore
dashjs.MediaPlayer = origMediaPlayer;
videojs.xhr = origVJSXHR;
},
setLimitBitrateByPortal(value) {
setLimitBitrateByPortalCalled = true;
setLimitBitrateByPortalValue = value;
},
on(event, fn) {
if (!eventHandlers[event]) {
eventHandlers[event] = [];
}
eventHandlers[event].push(fn);
},
reset: config.resetCallback,
trigger(event, data) {
if (!eventHandlers[event]) {
return;
}
eventHandlers[event].forEach(function(handler) {
handler(data);
});
},
setTextDefaultEnabled(value) {
setTextDefaultEnabledCalled = true;
setTextDefaultEnabledValue = value;
}
};
}
};
};
dashjs.MediaPlayer.events = origMediaPlayer.events;
const dashSourceHandler = Html5.selectSourceHandler(source);
return dashSourceHandler.handleSource(source, tech, options);
};
QUnit.module('videojs-dash dash.js SourceHandler', {
afterEach() {
videojs.Html5DashJS.hooks_ = {};
sampleSrc = {
src: 'movie.mpd',
type: 'application/dash+xml',
keySystemOptions: [{
name: 'com.widevine.alpha',
options: {
extra: 'data',
licenseUrl: 'https://example.com/license'
}
}]
};
}
});
QUnit.test('validate the Dash.js SourceHandler in Html5', function(assert) {
const dashSource = {
src: 'some.mpd',
type: 'application/dash+xml'
};
const maybeDashSource = {
src: 'some.mpd'
};
const nonDashSource = {
src: 'some.mp4',
type: 'video/mp4'
};
const dashSourceHandler = videojs.getTech('Html5').selectSourceHandler(dashSource);
assert.ok(dashSourceHandler, 'A DASH handler was found');
assert.strictEqual(dashSourceHandler.canHandleSource(dashSource), 'probably',
'canHandleSource with proper mime-type returns "probably"');
assert.strictEqual(dashSourceHandler.canHandleSource(maybeDashSource), 'maybe',
'canHandleSource with expected extension returns "maybe"');
assert.strictEqual(dashSourceHandler.canHandleSource(nonDashSource), '',
'canHandleSource with anything else returns ""');
assert.strictEqual(dashSourceHandler.canPlayType(dashSource.type), 'probably',
'canPlayType with proper mime-type returns "probably"');
assert.strictEqual(dashSourceHandler.canPlayType(nonDashSource.type), '',
'canPlayType with anything else returns ""');
});
QUnit.test('validate buildDashJSProtData function', function(assert) {
const output = videojs.Html5DashJS.buildDashJSProtData(sampleSrc.keySystemOptions);
const empty = videojs.Html5DashJS.buildDashJSProtData(undefined);
assert.strictEqual(output['com.widevine.alpha'].serverURL, 'https://example.com/license',
'licenceUrl converted to serverURL');
assert.equal(empty, null, 'undefined keySystemOptions returns null');
});
QUnit.test('validate handleSource function with src-provided key options', function(assert) {
const mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
testHandleSource(assert, sampleSrc, mergedKeySystemOptions);
});
QUnit.test('validate handleSource function with "limit bitrate by portal" option', function(assert) {
const mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {limitBitrateByPortal: true});
});
QUnit.test('validate handleSource function with invalid manifest', function(assert) {
const mergedKeySystemOptions = null;
testHandleSource(assert, sampleSrcNoDRM, mergedKeySystemOptions);
});
QUnit.test('update the source keySystemOptions', function(assert) {
const mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
},
'com.widevine.alpha1': {
serverURL: 'https://example.com/anotherlicense'
}
};
const updateSourceData = function(source) {
const numOfKeySystems = source.keySystemOptions.length;
source.keySystemOptions.push({
name: 'com.widevine.alpha' + numOfKeySystems,
options: {
serverURL: 'https://example.com/anotherlicense'
}
});
return source;
};
videojs.Html5DashJS.hook('updatesource', updateSourceData);
testHandleSource(assert, sampleSrc, mergedKeySystemOptions);
});
QUnit.test('registers hook callbacks correctly', function(assert) {
let cb1Count = 0;
let cb2Count = 0;
const cb1 = function(source) {
cb1Count++;
return source;
};
const cb2 = function() {
cb2Count++;
};
const mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
videojs.Html5DashJS.hook('updatesource', cb1);
videojs.Html5DashJS.hook('beforeinitialize', cb2);
testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {limitBitrateByPortal: true});
assert.expect(11);
assert.equal(cb1Count, 2,
'registered first callback and called');
assert.equal(cb2Count, 1,
'registered second callback and called');
});
QUnit.test('removes callbacks with removeInitializationHook correctly', function(assert) {
let cb1Count = 0;
let cb2Count = 0;
let cb3Count = 0;
let cb4Count = 0;
const cb1 = function() {
cb1Count++;
};
const cb2 = function() {
cb2Count++;
assert.ok(videojs.Html5DashJS.removeHook('beforeinitialize', cb2),
'removed hook cb2');
};
const cb3 = function(source) {
cb3Count++;
return source;
};
const cb4 = function(source) {
cb4Count++;
return source;
};
const mergedKeySystemOptions = {
'com.widevine.alpha': {
extra: 'data',
serverURL: 'https://example.com/license'
}
};
videojs.Html5DashJS.hook('beforeinitialize', [cb1, cb2]);
videojs.Html5DashJS.hook('updatesource', [cb3, cb4]);
assert.equal(videojs.Html5DashJS.hooks('beforeinitialize').length, 2,
'added 2 hooks to beforeinitialize');
assert.equal(videojs.Html5DashJS.hooks('updatesource').length, 2,
'added 2 hooks to updatesource');
assert.ok(!videojs.Html5DashJS.removeHook('beforeinitialize', cb3),
'nothing removed if callback not found');
assert.ok(videojs.Html5DashJS.removeHook('updatesource', cb3), 'removed cb3');
assert.equal(videojs.Html5DashJS.hooks('updatesource').length, 1, 'removed hook cb3');
testHandleSource(assert, sampleSrc, mergedKeySystemOptions, {limitBitrateByPortal: true});
assert.expect(20);
assert.equal(cb1Count, 1, 'called cb1');
assert.equal(cb2Count, 1, 'called cb2');
assert.equal(cb3Count, 0, 'did not call cb3');
assert.equal(cb4Count, 2, 'called cb4');
assert.equal(videojs.Html5DashJS.hooks('beforeinitialize').length, 1,
'cb2 removed itself');
});
QUnit.test('attaches dash.js error handler', function(assert) {
const eventHandlers = {};
const sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, {eventHandlers});
assert.expect(10);
assert.equal(eventHandlers[dashjs.MediaPlayer.events.ERROR][0],
sourceHandler.retriggerError_);
});
QUnit.test('handles various errors', function(assert) {
const errors = [
{
receive: {error: 'capability', event: 'mediasource'},
trigger: {code: 4, message: 'The media cannot be played because it requires a feature ' +
'that your browser does not support.'}
},
{
receive: {error: 'manifestError',
event: {id: 'createParser', message: 'manifest type unsupported'}},
trigger: {code: 4, message: 'manifest type unsupported'}
},
{
receive: {error: 'manifestError',
event: {id: 'codec', message: 'Codec (h264) is not supported'}},
trigger: {code: 4, message: 'Codec (h264) is not supported'}
},
{
receive: {error: 'manifestError',
event: {id: 'nostreams', message: 'No streams to play.'}},
trigger: {code: 4, message: 'No streams to play.'}
},
{
receive: {error: 'manifestError',
event: {id: 'nostreamscomposed', message: 'Error creating stream.'}},
trigger: {code: 4, message: 'Error creating stream.'}
},
{
receive: {error: 'manifestError',
event: {id: 'parse', message: 'parsing the manifest failed'}},
trigger: {code: 4, message: 'parsing the manifest failed'}
},
{
receive: {error: 'manifestError',
event: {id: 'nostreams', message: 'Multiplexed representations are intentionally not ' +
'supported, as they are not compliant with the DASH-AVC/264 guidelines'}},
trigger: {code: 4, message: 'Multiplexed representations are intentionally not ' +
'supported, as they are not compliant with the DASH-AVC/264 guidelines'}
},
{
receive: {error: 'mediasource', event: 'MEDIA_ERR_ABORTED: Some context'},
trigger: {code: 1, message: 'MEDIA_ERR_ABORTED: Some context'}
},
{
receive: {error: 'mediasource', event: 'MEDIA_ERR_NETWORK: Some context'},
trigger: {code: 2, message: 'MEDIA_ERR_NETWORK: Some context'}
},
{
receive: {error: 'mediasource', event: 'MEDIA_ERR_DECODE: Some context'},
trigger: {code: 3, message: 'MEDIA_ERR_DECODE: Some context'}
},
{
receive: {error: 'mediasource', event: 'MEDIA_ERR_SRC_NOT_SUPPORTED: Some context'},
trigger: {code: 4, message: 'MEDIA_ERR_SRC_NOT_SUPPORTED: Some context'}
},
{
receive: {error: 'mediasource', event: 'MEDIA_ERR_ENCRYPTED: Some context'},
trigger: {code: 5, message: 'MEDIA_ERR_ENCRYPTED: Some context'}
},
{
receive: {error: 'mediasource', event: 'UNKNOWN: Some context'},
trigger: {code: 4, message: 'UNKNOWN: Some context'}
},
{
receive: {error: 'mediasource', event: 'Error creating video source buffer'},
trigger: {code: 4, message: 'Error creating video source buffer'}
},
{
receive: {error: 'capability', event: 'encryptedmedia'},
trigger: {code: 5, message: 'The media cannot be played because it requires encryption ' +
'features that your browser does not support.'}
},
{
receive: {error: 'key_session', event: 'Some encryption error'},
trigger: {code: 5, message: 'Some encryption error'}
},
{
receive: {error: 'download', event: { id: 'someId', url: 'http://some/url', request: {} }},
trigger: {code: 2, message: 'The media playback was aborted because too many ' +
'consecutive download errors occurred.'}
},
{
receive: {error: 'mssError', event: 'MSS_NO_TFRF : Missing tfrf in live media segment'},
trigger: {code: 3, message: 'MSS_NO_TFRF : Missing tfrf in live media segment'}
}
];
// Make sure the MediaPlayer gets reset enough times
const done = assert.async(errors.length);
const resetCallback = function() {
done();
};
const eventHandlers = {};
const sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null,
{eventHandlers, resetCallback});
assert.expect(9 + (errors.length * 2));
let i;
sourceHandler.player.on('error', function() {
assert.equal(sourceHandler.player.error().code, errors[i].trigger.code, 'error code matches');
assert.equal(sourceHandler.player.error().message, errors[i].trigger.message,
'error message matches');
});
// dispatch all handled errors and see if they throw the correct details
for (i = 0; i < errors.length; i++) {
sourceHandler.mediaPlayer_.trigger(dashjs.MediaPlayer.events.ERROR, errors[i].receive);
}
});
QUnit.test('ignores unknown errors', function(assert) {
let resetCalled = false;
const resetCallback = () => {
resetCalled = true;
};
const sourceHandler = testHandleSource(assert, sampleSrcNoDRM, null, {resetCallback});
const done = assert.async(1);
sourceHandler.mediaPlayer_.trigger(dashjs.MediaPlayer.events.ERROR, {error: 'unknown'});
assert.equal(sourceHandler.player.error(), null, 'No error dispatched');
// The error handler waits 10ms before firing reset, so we wait for
// 20ms here to make sure it doesn't fire
setTimeout(function() {
assert.notOk(resetCalled, 'MediaPlayer has not been reset');
done();
}, 20);
assert.expect(11);
});