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.
 
 
 

438 lines
13 KiB

import QUnit from 'qunit';
import { default as DashPlaylistLoader, updateMaster } from '../src/dash-playlist-loader';
import xhrFactory from '../src/xhr';
import {
useFakeEnvironment,
standardXHRResponse,
urlTo
} from './test-helpers';
QUnit.module('DASH Playlist Loader', {
beforeEach(assert) {
this.env = useFakeEnvironment(assert);
this.clock = this.env.clock;
this.requests = this.env.requests;
this.fakeHls = {
xhr: xhrFactory()
};
},
afterEach() {
this.env.restore();
}
});
QUnit.test('throws if the playlist url is empty or undefined', function(assert) {
assert.throws(function() {
DashPlaylistLoader();
}, 'requires an argument');
assert.throws(function() {
DashPlaylistLoader('');
}, 'does not accept the empty string');
});
QUnit.test('starts with a manifest URL or playlist', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
assert.notOk(loader.started, 'not started');
loader.load();
assert.equal(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
assert.ok(loader.started, 'started');
loader.master = { playlists: { 'playlist-1': { endList: true } }, mediaGroups: {} };
loader.parseMasterXml = () => {
return { playlists: [], mediaGroups: {} };
};
let newLoader =
new DashPlaylistLoader({ uri: 'playlist-1' }, this.fakeHls, false, loader);
assert.equal(newLoader.state, 'HAVE_METADATA', 'has metadata');
assert.ok(newLoader.started, 'started');
});
QUnit.test('requests the manifest immediately when given a URL', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
assert.equal(this.requests.length, 1, 'made a request');
assert.equal(this.requests[0].url, 'dash.mpd', 'requested the manifest');
});
QUnit.test('moves to HAVE_MASTER and HAVE_METADATA after loading the manifest',
function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
let loadedPlaylistStates = [];
loader.load();
loader.on('loadedplaylist', function() {
loadedPlaylistStates.push(loader.state);
});
standardXHRResponse(this.requests.shift());
assert.ok(loader.master, 'the master playlist is available');
// because DASH only has one manifest, it should go through two loadedplaylists
// and end with HAVE_METADATA because it already has the first media ready
assert.equal(loadedPlaylistStates.length, 2, 'triggered two loadedplaylist events');
assert.equal(loadedPlaylistStates[0], 'HAVE_MASTER', 'got master first');
assert.equal(loadedPlaylistStates[1], 'HAVE_METADATA', 'got media second');
});
QUnit.test('throws an error when initial manifest request fails', function(assert) {
let errors = [];
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
loader.on('error', function() {
errors.push(loader.error);
});
this.requests.pop().respond(500);
assert.equal(errors.length, 1, 'threw an error');
assert.equal(errors[0].status, 500, 'captured http status');
});
QUnit.test('throws an error if a media switch is initiated too early', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
assert.throws(
function() {
loader.media('1080p');
},
new Error('Cannot switch media playlist from HAVE_NOTHING'),
'threw an error from HAVE_NOTHING');
});
QUnit.test('throws an error if a switch to an unrecognized playlist is requested',
function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
standardXHRResponse(this.requests.shift());
assert.throws(function() {
loader.media('unrecognized');
}, new Error('Unknown playlist URI: unrecognized'), 'throws an error');
});
QUnit.test('can switch playlists after the master is downloaded', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
// first media will already be selected since DASH needs no media request, so change on
// loadedmetadata
loader.on('loadedmetadata', function() {
loader.media('placeholder-uri-0');
});
standardXHRResponse(this.requests.shift());
assert.equal(loader.media().uri, 'placeholder-uri-0', 'changed to new playlist');
loader.media('placeholder-uri-1');
assert.equal(loader.media().uri, 'placeholder-uri-1', 'changed to new playlist');
});
QUnit.test('can switch playlists based on object or URI', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
standardXHRResponse(this.requests.shift());
loader.media('placeholder-uri-0');
assert.equal(loader.media().uri, 'placeholder-uri-0', 'changed to playlist by uri');
loader.media('placeholder-uri-1');
assert.equal(loader.media().uri, 'placeholder-uri-1', 'changed to playlist by uri');
loader.media(loader.master.playlists[0]);
assert.equal(loader.media().uri, 'placeholder-uri-0', 'changed to playlist by object');
});
QUnit.test('dispose aborts pending manifest request', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
assert.equal(this.requests.length, 1, 'one request');
assert.notOk(this.requests[0].aborted, 'request not aborted');
assert.ok(this.requests[0].onreadystatechange, 'onreadystatechange handler exists');
loader.dispose();
assert.equal(this.requests.length, 1, 'one request');
assert.ok(this.requests[0].aborted, 'request aborted');
assert.notOk(this.requests[0].onreadystatechange,
'onreadystatechange handler does not exist');
});
QUnit.test('errors if requests take longer than 45s', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
let errors = 0;
loader.load();
loader.on('error', function() {
errors++;
});
this.clock.tick(45 * 1000);
assert.strictEqual(errors, 1, 'fired one error');
assert.strictEqual(loader.error.code, 2, 'fired a network error');
});
QUnit.test('triggers an event when the active media changes', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
let mediaChanges = 0;
let mediaChangings = 0;
loader.load();
loader.on('mediachange', function() {
mediaChanges++;
});
loader.on('mediachanging', function() {
mediaChangings++;
});
standardXHRResponse(this.requests.shift());
assert.strictEqual(mediaChangings, 0,
'initial selection does not fire a mediachanging event');
assert.strictEqual(mediaChanges, 0,
'initial selection does not fire a mediachange event');
loader.media(loader.master.playlists[1]);
assert.strictEqual(mediaChangings, 1, 'fired a mediachanging event');
assert.strictEqual(mediaChanges, 1, 'fired a mediachange event');
loader.media(loader.master.playlists[0]);
assert.strictEqual(mediaChangings, 2, 'fired a mediachanging event');
assert.strictEqual(mediaChanges, 2, 'fired a mediachange');
// no op switch
loader.media(loader.master.playlists[0]);
assert.strictEqual(mediaChangings, 2, 'ignored the no-op media change');
assert.strictEqual(mediaChanges, 2, 'ignored the no-op media change');
});
QUnit.test('parseMasterXml parses master manifest and sets up uri references',
function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
loader.load();
standardXHRResponse(this.requests.shift());
assert.equal(loader.master.playlists[0].uri, 'placeholder-uri-0',
'setup phony uri for media playlist');
assert.strictEqual(loader.master.playlists['placeholder-uri-0'],
loader.master.playlists[0], 'set reference by uri for easy access');
assert.equal(loader.master.playlists[1].uri, 'placeholder-uri-1',
'setup phony uri for media playlist');
assert.strictEqual(loader.master.playlists['placeholder-uri-1'],
loader.master.playlists[1], 'set reference by uri for easy access');
assert.equal(loader.master.mediaGroups.AUDIO.audio.main.playlists[0].uri,
'placeholder-uri-AUDIO-audio-main', 'setup phony uri for media groups');
assert.strictEqual(loader.master.playlists['placeholder-uri-AUDIO-audio-main'],
loader.master.mediaGroups.AUDIO.audio.main.playlists[0],
'set reference by uri for easy access');
});
QUnit.test('updateMaster updates playlists and mediaGroups', function(assert) {
const master = {
duration: 10,
minimumUpdatePeriod: 0,
mediaGroups: {
AUDIO: {
audio: {
main: {
playlists: [{
mediaSequence: 0,
attributes: {},
uri: 'audio-0-uri',
resolvedUri: urlTo('audio-0-uri'),
segments: [{
duration: 10,
uri: 'audio-segment-0-uri',
resolvedUri: urlTo('audio-segment-0-uri')
}]
}]
}
}
}
},
playlists: [{
mediaSequence: 0,
attributes: {
BANDWIDTH: 9
},
uri: 'playlist-0-uri',
resolvedUri: urlTo('playlist-0-uri'),
segments: [{
duration: 10,
uri: 'segment-0-uri',
resolvedUri: urlTo('segment-0-uri')
}]
}]
};
const update = {
duration: 20,
minimumUpdatePeriod: 0,
mediaGroups: {
AUDIO: {
audio: {
main: {
playlists: [{
mediaSequence: 1,
attributes: {},
uri: 'audio-0-uri',
resolvedUri: urlTo('audio-0-uri'),
segments: [{
duration: 10,
uri: 'audio-segment-0-uri',
resolvedUri: urlTo('audio-segment-0-uri')
}]
}]
}
}
}
},
playlists: [{
mediaSequence: 1,
attributes: {
BANDWIDTH: 9
},
uri: 'playlist-0-uri',
resolvedUri: urlTo('playlist-0-uri'),
segments: [{
duration: 10,
uri: 'segment-0-uri',
resolvedUri: urlTo('segment-0-uri')
}]
}]
};
master.playlists['playlist-0-uri'] = master.playlists[0];
master.playlists['audio-0-uri'] = master.mediaGroups.AUDIO.audio.main.playlists[0];
assert.deepEqual(
updateMaster(master, update),
{
duration: 20,
minimumUpdatePeriod: 0,
mediaGroups: {
AUDIO: {
audio: {
main: {
playlists: [{
mediaSequence: 1,
attributes: {},
uri: 'audio-0-uri',
resolvedUri: urlTo('audio-0-uri'),
segments: [{
duration: 10,
uri: 'audio-segment-0-uri',
resolvedUri: urlTo('audio-segment-0-uri')
}]
}]
}
}
}
},
playlists: [{
mediaSequence: 1,
attributes: {
BANDWIDTH: 9
},
uri: 'playlist-0-uri',
resolvedUri: urlTo('playlist-0-uri'),
segments: [{
duration: 10,
uri: 'segment-0-uri',
resolvedUri: urlTo('segment-0-uri')
}]
}]
},
'updates playlists and media groups');
});
QUnit.test('refreshes the xml if there is a minimumUpdatePeriod', function(assert) {
let loader = new DashPlaylistLoader('dash-live.mpd', this.fakeHls);
let minimumUpdatePeriods = 0;
loader.on('minimumUpdatePeriod', () => minimumUpdatePeriods++);
loader.load();
assert.equal(minimumUpdatePeriods, 0, 'no refreshs to start');
standardXHRResponse(this.requests.shift());
assert.equal(minimumUpdatePeriods, 0, 'no refreshs immediately after response');
this.clock.tick(4 * 1000);
assert.equal(this.requests.length, 1, 'refreshed manifest');
assert.equal(this.requests[0].uri, 'dash-live.mpd', 'refreshed manifest');
assert.equal(minimumUpdatePeriods, 1, 'refreshed manifest');
});
QUnit.test('media playlists "refresh" by re-parsing master xml', function(assert) {
let loader = new DashPlaylistLoader('dash-live.mpd', this.fakeHls);
const parseMasterXml_ = loader.parseMasterXml.bind(loader);
let refreshes = 0;
loader.on('mediaupdatetimeout', () => refreshes++);
loader.parseMasterXml = () => {
const result = parseMasterXml_();
// add segment to segment list for proper refresh delay functionality
result.playlists[0].segments.push({ duration: 2, uri: 'segment-0' });
return result;
};
loader.load();
standardXHRResponse(this.requests.shift());
// 2s, last segment duration
this.clock.tick(2 * 1000);
assert.equal(refreshes, 1, 'refreshed playlist after last segment duration');
});
QUnit.test('delays load when on final rendition', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);
let loadedplaylistEvents = 0;
loader.on('loadedplaylist', () => loadedplaylistEvents++);
// do an initial load to start the loader
loader.load();
standardXHRResponse(this.requests.shift());
// one for master, one for media on first selection
assert.equal(loadedplaylistEvents, 2, 'two loadedplaylist events after first load');
loader.load();
assert.equal(loadedplaylistEvents, 3, 'one more loadedplaylist event after load');
loader.load(false);
assert.equal(
loadedplaylistEvents,
4,
'one more loadedplaylist event after load with isFinalRendition false');
loader.load(true);
assert.equal(
loadedplaylistEvents,
4,
'no loadedplaylist event after load with isFinalRendition false');
this.clock.tick(loader.media().targetDuration / 2 * 1000);
assert.equal(
loadedplaylistEvents,
5,
'one more loadedplaylist event after final rendition delay');
});