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.
956 lines
31 KiB
956 lines
31 KiB
import QUnit from 'qunit';
|
|
import videojs from 'video.js';
|
|
import {
|
|
useFakeEnvironment,
|
|
useFakeMediaSource,
|
|
createPlayer,
|
|
standardXHRResponse,
|
|
openMediaSource
|
|
} from './test-helpers.js';
|
|
import manifests from './test-manifests.js';
|
|
import {
|
|
MasterPlaylistController,
|
|
mimeTypesForPlaylist_
|
|
} from '../src/master-playlist-controller';
|
|
/* eslint-disable no-unused-vars */
|
|
// we need this so that it can register hls with videojs
|
|
import { Hls } from '../src/videojs-contrib-hls';
|
|
/* eslint-enable no-unused-vars */
|
|
import Playlist from '../src/playlist';
|
|
|
|
QUnit.module('MasterPlaylistController', {
|
|
beforeEach(assert) {
|
|
this.env = useFakeEnvironment(assert);
|
|
this.clock = this.env.clock;
|
|
this.requests = this.env.requests;
|
|
this.mse = useFakeMediaSource();
|
|
|
|
// force the HLS tech to run
|
|
this.origSupportsNativeHls = videojs.Hls.supportsNativeHls;
|
|
videojs.Hls.supportsNativeHls = false;
|
|
this.oldFirefox = videojs.browser.IS_FIREFOX;
|
|
this.player = createPlayer();
|
|
this.player.src({
|
|
src: 'manifest/master.m3u8',
|
|
type: 'application/vnd.apple.mpegurl'
|
|
});
|
|
this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
|
|
},
|
|
afterEach() {
|
|
this.env.restore();
|
|
this.mse.restore();
|
|
videojs.Hls.supportsNativeHls = this.origSupportsNativeHls;
|
|
videojs.browser.IS_FIREFOX = this.oldFirefox;
|
|
this.player.dispose();
|
|
}
|
|
});
|
|
|
|
QUnit.test('throws error when given an empty URL', function(assert) {
|
|
let options = {
|
|
url: 'test',
|
|
tech: this.player.tech_
|
|
};
|
|
|
|
assert.ok(new MasterPlaylistController(options), 'can create with options');
|
|
|
|
options.url = '';
|
|
assert.throws(() => {
|
|
new MasterPlaylistController(options); // eslint-disable-line no-new
|
|
}, /A non-empty playlist URL is required/, 'requires a non empty url');
|
|
});
|
|
|
|
QUnit.test('obeys none preload option', function(assert) {
|
|
this.player.preload('none');
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// playlist
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
assert.equal(this.requests.length, 0, 'no segment requests');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
|
|
});
|
|
|
|
QUnit.test('obeys auto preload option', function(assert) {
|
|
this.player.preload('auto');
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// playlist
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
assert.equal(this.requests.length, 1, '1 segment request');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
|
|
});
|
|
|
|
QUnit.test('obeys metadata preload option', function(assert) {
|
|
this.player.preload('metadata');
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// playlist
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
assert.equal(this.requests.length, 1, '1 segment request');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
|
|
});
|
|
|
|
QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
|
|
let resyncs = 0;
|
|
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
this.masterPlaylistController.mediaSource.trigger('sourceopen');
|
|
|
|
let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
|
|
|
|
segmentLoader.resyncLoader = function() {
|
|
resyncs++;
|
|
};
|
|
|
|
this.masterPlaylistController.selectPlaylist = () => {
|
|
return this.masterPlaylistController.master().playlists[0];
|
|
};
|
|
|
|
this.masterPlaylistController.fastQualityChange_();
|
|
|
|
assert.equal(resyncs, 1, 'resynced the segmentLoader');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
|
|
});
|
|
|
|
QUnit.test('does not resync the segmentLoader when no fast quality change occurs',
|
|
function(assert) {
|
|
let resyncs = 0;
|
|
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
this.masterPlaylistController.mediaSource.trigger('sourceopen');
|
|
|
|
let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
|
|
|
|
segmentLoader.resyncLoader = function() {
|
|
resyncs++;
|
|
};
|
|
|
|
this.masterPlaylistController.fastQualityChange_();
|
|
|
|
assert.equal(resyncs, 0, 'did not resync the segmentLoader');
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
|
|
});
|
|
|
|
QUnit.test('if buffered, will request second segment byte range', function(assert) {
|
|
this.requests.length = 0;
|
|
this.player.src({
|
|
src: 'manifest/playlist.m3u8',
|
|
type: 'application/vnd.apple.mpegurl'
|
|
});
|
|
this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
|
|
|
|
// mock that the user has played the video before
|
|
this.player.tech_.triggerReady();
|
|
this.clock.tick(1);
|
|
this.player.tech_.trigger('play');
|
|
this.player.tech_.paused_ = false;
|
|
this.player.tech_.played = () => videojs.createTimeRanges([[0, 20]]);
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
// playlist
|
|
standardXHRResponse(this.requests[0]);
|
|
|
|
this.masterPlaylistController.mainSegmentLoader_.sourceUpdater_.buffered = () => {
|
|
return videojs.createTimeRanges([[0, 20]]);
|
|
};
|
|
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
|
|
this.clock.tick(1);
|
|
// segment
|
|
standardXHRResponse(this.requests[1]);
|
|
this.masterPlaylistController.mainSegmentLoader_.fetchAtBuffer_ = true;
|
|
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
|
|
this.clock.tick(10 * 1000);
|
|
assert.equal(this.requests[2].headers.Range, 'bytes=522828-1110327');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
|
|
assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
|
|
assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
|
|
1024,
|
|
'1024 bytes downloaded');
|
|
});
|
|
|
|
QUnit.test('re-initializes the combined playlist loader when switching sources',
|
|
function(assert) {
|
|
openMediaSource(this.player, this.clock);
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// playlist
|
|
standardXHRResponse(this.requests.shift());
|
|
// segment
|
|
standardXHRResponse(this.requests.shift());
|
|
// change the source
|
|
this.player.src({
|
|
src: 'manifest/master.m3u8',
|
|
type: 'application/vnd.apple.mpegurl'
|
|
});
|
|
this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
|
|
// maybe not needed if https://github.com/videojs/video.js/issues/2326 gets fixed
|
|
this.clock.tick(1);
|
|
assert.ok(!this.masterPlaylistController.masterPlaylistLoader_.media(),
|
|
'no media playlist');
|
|
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.state,
|
|
'HAVE_NOTHING',
|
|
'reset the playlist loader state');
|
|
assert.equal(this.requests.length, 1, 'requested the new src');
|
|
|
|
// buffer check
|
|
this.clock.tick(10 * 1000);
|
|
assert.equal(this.requests.length, 1, 'did not request a stale segment');
|
|
|
|
// sourceopen
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
assert.equal(this.requests.length, 1, 'made one request');
|
|
assert.ok(
|
|
this.requests[0].url.indexOf('master.m3u8') >= 0,
|
|
'requested only the new playlist'
|
|
);
|
|
});
|
|
|
|
QUnit.test('updates the combined segment loader on live playlist refreshes', function(assert) {
|
|
let updates = [];
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
|
|
updates.push(update);
|
|
};
|
|
|
|
this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
|
|
assert.equal(updates.length, 1, 'updated the segment list');
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
|
|
});
|
|
|
|
QUnit.test(
|
|
'fires a progress event after downloading a segment from combined segment loader',
|
|
function(assert) {
|
|
let progressCount = 0;
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
this.player.tech_.on('progress', function() {
|
|
progressCount++;
|
|
});
|
|
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
|
|
this.clock.tick(1);
|
|
// segment
|
|
standardXHRResponse(this.requests.shift());
|
|
this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
|
|
assert.equal(progressCount, 1, 'fired a progress event');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
|
|
assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
|
|
assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
|
|
1024,
|
|
'1024 bytes downloaded');
|
|
});
|
|
|
|
QUnit.test('updates the enabled track when switching audio groups', function(assert) {
|
|
openMediaSource(this.player, this.clock);
|
|
// master
|
|
this.requests.shift().respond(200, null,
|
|
manifests.multipleAudioGroupsCombinedMain);
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
// init segment
|
|
standardXHRResponse(this.requests.shift());
|
|
// video segment
|
|
standardXHRResponse(this.requests.shift());
|
|
// audio media
|
|
standardXHRResponse(this.requests.shift());
|
|
// ignore audio segment requests
|
|
this.requests.length = 0;
|
|
|
|
let mpc = this.masterPlaylistController;
|
|
let combinedPlaylist = mpc.master().playlists[0];
|
|
|
|
mpc.masterPlaylistLoader_.media(combinedPlaylist);
|
|
// updated media
|
|
this.requests.shift().respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXTINF:5.0\n' +
|
|
'0.ts\n' +
|
|
'#EXT-X-ENDLIST\n');
|
|
|
|
assert.ok(mpc.activeAudioGroup().filter((track) => track.enabled)[0],
|
|
'enabled a track in the new audio group');
|
|
});
|
|
|
|
QUnit.test('blacklists switching from video+audio playlists to audio only', function(assert) {
|
|
let audioPlaylist;
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
this.player.tech_.hls.bandwidth = 1e10;
|
|
|
|
// master
|
|
this.requests.shift().respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
|
|
'media.m3u8\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
|
|
'media1.m3u8\n');
|
|
// media1
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
|
|
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1],
|
|
'selected video+audio');
|
|
audioPlaylist = this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0];
|
|
assert.equal(audioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth we set above');
|
|
});
|
|
|
|
QUnit.test('blacklists switching from audio-only playlists to video+audio', function(assert) {
|
|
let videoAudioPlaylist;
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
this.player.tech_.hls.bandwidth = 1;
|
|
// master
|
|
this.requests.shift().respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
|
|
'media.m3u8\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
|
|
'media1.m3u8\n');
|
|
|
|
// media1
|
|
standardXHRResponse(this.requests.shift());
|
|
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
|
|
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
|
|
'selected audio only');
|
|
videoAudioPlaylist =
|
|
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
|
|
assert.equal(videoAudioPlaylist.excludeUntil,
|
|
Infinity,
|
|
'excluded incompatible playlist');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
|
|
});
|
|
|
|
QUnit.test('blacklists switching from video-only playlists to video+audio', function(assert) {
|
|
let videoAudioPlaylist;
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
this.player.tech_.hls.bandwidth = 1;
|
|
// master
|
|
this.requests.shift()
|
|
.respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d"\n' +
|
|
'media.m3u8\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
|
|
'media1.m3u8\n');
|
|
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
|
|
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
|
|
'selected video only');
|
|
videoAudioPlaylist =
|
|
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
|
|
assert.equal(videoAudioPlaylist.excludeUntil,
|
|
Infinity,
|
|
'excluded incompatible playlist');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
|
|
});
|
|
|
|
QUnit.test('blacklists switching between playlists with incompatible audio codecs',
|
|
function(assert) {
|
|
let alternatePlaylist;
|
|
|
|
openMediaSource(this.player, this.clock);
|
|
|
|
this.player.tech_.hls.bandwidth = 1;
|
|
// master
|
|
this.requests.shift()
|
|
.respond(200, null,
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
|
|
'media.m3u8\n' +
|
|
'#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
|
|
'media1.m3u8\n');
|
|
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
|
|
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
|
|
'selected HE-AAC stream');
|
|
alternatePlaylist =
|
|
this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
|
|
assert.equal(alternatePlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
|
|
});
|
|
|
|
QUnit.test('blacklists the current playlist when audio changes in Firefox 48 & below',
|
|
function(assert) {
|
|
videojs.browser.IS_FIREFOX = true;
|
|
|
|
let origSupportsAudioInfoChange_ = videojs.Hls.supportsAudioInfoChange_;
|
|
|
|
videojs.Hls.supportsAudioInfoChange_ = () => false;
|
|
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
let media = this.masterPlaylistController.media();
|
|
|
|
// initial audio config
|
|
this.masterPlaylistController.mediaSource.trigger({
|
|
type: 'audioinfo',
|
|
info: {}
|
|
});
|
|
// updated audio config
|
|
|
|
this.masterPlaylistController.mediaSource.trigger({
|
|
type: 'audioinfo',
|
|
info: {
|
|
different: true
|
|
}
|
|
});
|
|
assert.ok(media.excludeUntil > 0, 'blacklisted the old playlist');
|
|
assert.equal(this.env.log.warn.callCount, 2, 'logged two warnings');
|
|
this.env.log.warn.callCount = 0;
|
|
videojs.Hls.supportsAudioInfoChange_ = origSupportsAudioInfoChange_;
|
|
});
|
|
|
|
QUnit.test('updates the combined segment loader on media changes', function(assert) {
|
|
let updates = [];
|
|
|
|
this.masterPlaylistController.mediaSource.trigger('sourceopen');
|
|
|
|
this.masterPlaylistController.mainSegmentLoader_.bandwidth = 1;
|
|
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
|
|
updates.push(update);
|
|
};
|
|
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
|
|
this.clock.tick(1);
|
|
|
|
// downloading the new segment will update bandwidth and cause a
|
|
// playlist change
|
|
// segment 0
|
|
standardXHRResponse(this.requests.shift());
|
|
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
assert.ok(updates.length > 0, 'updated the segment list');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
|
|
assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
|
|
assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
|
|
1024,
|
|
'1024 bytes downloaded');
|
|
});
|
|
|
|
QUnit.test('selects a playlist after main/combined segment downloads', function(assert) {
|
|
let calls = 0;
|
|
|
|
this.masterPlaylistController.selectPlaylist = () => {
|
|
calls++;
|
|
return this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0];
|
|
};
|
|
this.masterPlaylistController.mediaSource.trigger('sourceopen');
|
|
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
// "downloaded" a segment
|
|
this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
|
|
assert.strictEqual(calls, 2, 'selects after the initial segment');
|
|
|
|
// and another
|
|
this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
|
|
assert.strictEqual(calls, 3, 'selects after additional segments');
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
|
|
});
|
|
|
|
QUnit.test('updates the duration after switching playlists', function(assert) {
|
|
let selectedPlaylist = false;
|
|
|
|
this.masterPlaylistController.mediaSource.trigger('sourceopen');
|
|
|
|
this.masterPlaylistController.bandwidth = 1e20;
|
|
|
|
// master
|
|
standardXHRResponse(this.requests[0]);
|
|
// media
|
|
standardXHRResponse(this.requests[1]);
|
|
|
|
this.masterPlaylistController.selectPlaylist = () => {
|
|
selectedPlaylist = true;
|
|
|
|
// this duration should be overwritten by the playlist change
|
|
this.masterPlaylistController.mediaSource.duration = 0;
|
|
this.masterPlaylistController.mediaSource.readyState = 'open';
|
|
|
|
return this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
|
|
};
|
|
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
|
|
this.clock.tick(1);
|
|
// segment 0
|
|
standardXHRResponse(this.requests[2]);
|
|
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
|
|
// media1
|
|
standardXHRResponse(this.requests[3]);
|
|
assert.ok(selectedPlaylist, 'selected playlist');
|
|
assert.ok(this.masterPlaylistController.mediaSource.duration !== 0,
|
|
'updates the duration');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
|
|
assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
|
|
assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
|
|
1024,
|
|
'1024 bytes downloaded');
|
|
});
|
|
|
|
QUnit.test('playlist selection uses systemBandwidth', function(assert) {
|
|
this.masterPlaylistController.mediaSource.trigger('sourceopen');
|
|
this.player.width(1000);
|
|
this.player.height(900);
|
|
|
|
// master
|
|
standardXHRResponse(this.requests[0]);
|
|
// media
|
|
standardXHRResponse(this.requests[1]);
|
|
assert.ok(/media3\.m3u8/i.test(this.requests[1].url), 'Selected the highest rendition');
|
|
|
|
// 1ms have passed to upload 1kb that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
|
|
this.clock.tick(1);
|
|
// segment 0
|
|
standardXHRResponse(this.requests[2]);
|
|
// 20ms have passed to upload 1kb that gives us a throughput of 1024 / 20 * 8 * 1000 = 409600
|
|
this.clock.tick(20);
|
|
this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
|
|
// systemBandwidth is 1 / (1 / 8192000 + 1 / 409600) = ~390095
|
|
|
|
// media1
|
|
standardXHRResponse(this.requests[3]);
|
|
assert.ok(/media\.m3u8/i.test(this.requests[3].url), 'Selected the rendition < 390095');
|
|
|
|
assert.ok(this.masterPlaylistController.mediaSource.duration !== 0,
|
|
'updates the duration');
|
|
|
|
// verify stats
|
|
assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
|
|
assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
|
|
assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
|
|
1024,
|
|
'1024 bytes downloaded');
|
|
});
|
|
|
|
QUnit.test('removes request timeout when segment timesout on lowest rendition',
|
|
function(assert) {
|
|
this.masterPlaylistController.mediaSource.trigger('sourceopen');
|
|
|
|
// master
|
|
standardXHRResponse(this.requests[0]);
|
|
// media
|
|
standardXHRResponse(this.requests[1]);
|
|
|
|
assert.equal(this.masterPlaylistController.requestOptions_.timeout,
|
|
this.masterPlaylistController.masterPlaylistLoader_.targetDuration * 1.5 *
|
|
1000,
|
|
'default request timeout');
|
|
|
|
assert.ok(!this.masterPlaylistController
|
|
.masterPlaylistLoader_
|
|
.isLowestEnabledRendition_(), 'Not lowest rendition');
|
|
|
|
// Cause segment to timeout to force player into lowest rendition
|
|
this.requests[2].timedout = true;
|
|
|
|
// Downloading segment should cause media change and timeout removal
|
|
// segment 0
|
|
standardXHRResponse(this.requests[2]);
|
|
// Download new segment after media change
|
|
standardXHRResponse(this.requests[3]);
|
|
|
|
assert.ok(this.masterPlaylistController
|
|
.masterPlaylistLoader_.isLowestEnabledRendition_(), 'On lowest rendition');
|
|
|
|
assert.equal(this.masterPlaylistController.requestOptions_.timeout, 0,
|
|
'request timeout 0');
|
|
});
|
|
|
|
QUnit.test('seekable uses the intersection of alternate audio and combined tracks',
|
|
function(assert) {
|
|
let origSeekable = Playlist.seekable;
|
|
let mpc = this.masterPlaylistController;
|
|
let mainMedia = {};
|
|
let audioMedia = {};
|
|
let mainTimeRanges = [];
|
|
let audioTimeRanges = [];
|
|
let assertTimeRangesEqual = (left, right, message) => {
|
|
if (left.length === 0 && right.length === 0) {
|
|
return;
|
|
}
|
|
|
|
assert.equal(left.length, 1, message);
|
|
assert.equal(right.length, 1, message);
|
|
|
|
assert.equal(left.start(0), right.start(0), message);
|
|
assert.equal(left.end(0), right.end(0), message);
|
|
};
|
|
|
|
this.masterPlaylistController.masterPlaylistLoader_.media = () => mainMedia;
|
|
|
|
Playlist.seekable = (media) => {
|
|
if (media === mainMedia) {
|
|
return videojs.createTimeRanges(mainTimeRanges);
|
|
}
|
|
return videojs.createTimeRanges(audioTimeRanges);
|
|
};
|
|
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges(),
|
|
'empty when main empty');
|
|
mainTimeRanges = [[0, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[0, 10]]),
|
|
'main when no audio');
|
|
|
|
mpc.audioPlaylistLoader_ = {
|
|
media: () => audioMedia,
|
|
dispose() {},
|
|
expired_: 0
|
|
};
|
|
mainTimeRanges = [];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges(),
|
|
'empty when both empty');
|
|
mainTimeRanges = [[0, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges(),
|
|
'empty when audio empty');
|
|
mainTimeRanges = [];
|
|
audioTimeRanges = [[0, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges(),
|
|
'empty when main empty');
|
|
mainTimeRanges = [[0, 10]];
|
|
audioTimeRanges = [[0, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[0, 10]]),
|
|
'ranges equal');
|
|
mainTimeRanges = [[5, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[5, 10]]),
|
|
'main later start');
|
|
mainTimeRanges = [[0, 10]];
|
|
audioTimeRanges = [[5, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[5, 10]]),
|
|
'audio later start');
|
|
mainTimeRanges = [[0, 9]];
|
|
audioTimeRanges = [[0, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[0, 9]]),
|
|
'main earlier end');
|
|
mainTimeRanges = [[0, 10]];
|
|
audioTimeRanges = [[0, 9]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[0, 9]]),
|
|
'audio earlier end');
|
|
mainTimeRanges = [[1, 10]];
|
|
audioTimeRanges = [[0, 9]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[1, 9]]),
|
|
'main later start, audio earlier end');
|
|
mainTimeRanges = [[0, 9]];
|
|
audioTimeRanges = [[1, 10]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[1, 9]]),
|
|
'audio later start, main earlier end');
|
|
mainTimeRanges = [[2, 9]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[2, 9]]),
|
|
'main later start, main earlier end');
|
|
mainTimeRanges = [[1, 10]];
|
|
audioTimeRanges = [[2, 9]];
|
|
mpc.seekable_ = videojs.createTimeRanges();
|
|
mpc.onSyncInfoUpdate_();
|
|
assertTimeRangesEqual(mpc.seekable(),
|
|
videojs.createTimeRanges([[2, 9]]),
|
|
'audio later start, audio earlier end');
|
|
|
|
Playlist.seekable = origSeekable;
|
|
});
|
|
|
|
QUnit.test('calls to update cues on new media', function(assert) {
|
|
let origHlsOptions = videojs.options.hls;
|
|
|
|
videojs.options.hls = {
|
|
useCueTags: true
|
|
};
|
|
|
|
this.player = createPlayer();
|
|
this.player.src({
|
|
src: 'manifest/media.m3u8',
|
|
type: 'application/vnd.apple.mpegurl'
|
|
});
|
|
this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
|
|
|
|
let callCount = 0;
|
|
|
|
this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
|
|
|
|
// master
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
assert.equal(callCount, 0, 'no call to update cues on master');
|
|
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
assert.equal(callCount, 1, 'calls to update cues on first media');
|
|
|
|
this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
|
|
|
|
assert.equal(callCount, 2, 'calls to update cues on subsequent media');
|
|
|
|
videojs.options.hls = origHlsOptions;
|
|
});
|
|
|
|
QUnit.test('calls to update cues on media when no master', function(assert) {
|
|
this.requests.length = 0;
|
|
|
|
this.player.src({
|
|
src: 'manifest/media.m3u8',
|
|
type: 'application/vnd.apple.mpegurl'
|
|
});
|
|
|
|
this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
|
|
this.masterPlaylistController.useCueTags_ = true;
|
|
|
|
let callCount = 0;
|
|
|
|
this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
|
|
|
|
// media
|
|
standardXHRResponse(this.requests.shift());
|
|
|
|
assert.equal(callCount, 1, 'calls to update cues on first media');
|
|
|
|
this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
|
|
|
|
assert.equal(callCount, 2, 'calls to update cues on subsequent media');
|
|
});
|
|
|
|
QUnit.test('respects useCueTags option', function(assert) {
|
|
let origHlsOptions = videojs.options.hls;
|
|
|
|
videojs.options.hls = {
|
|
useCueTags: true
|
|
};
|
|
|
|
this.player = createPlayer();
|
|
this.player.src({
|
|
src: 'manifest/media.m3u8',
|
|
type: 'application/vnd.apple.mpegurl'
|
|
});
|
|
this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
|
|
|
|
assert.ok(this.masterPlaylistController.cueTagsTrack_,
|
|
'creates cueTagsTrack_ if useCueTags is truthy');
|
|
assert.equal(this.masterPlaylistController.cueTagsTrack_.label,
|
|
'ad-cues',
|
|
'cueTagsTrack_ has label of ad-cues');
|
|
assert.equal(this.player.textTracks()[0], this.masterPlaylistController.cueTagsTrack_,
|
|
'adds cueTagsTrack as a text track if useCueTags is truthy');
|
|
|
|
videojs.options.hls = origHlsOptions;
|
|
});
|
|
|
|
QUnit.module('Codec to MIME Type Conversion');
|
|
|
|
QUnit.test('recognizes muxed codec configurations', function(assert) {
|
|
assert.deepEqual(mimeTypesForPlaylist_({ mediaGroups: {} }, {}),
|
|
[ 'video/mp2t; codecs="avc1.4d400d, mp4a.40.2"' ],
|
|
'returns a default MIME type when no codecs are present');
|
|
|
|
assert.deepEqual(mimeTypesForPlaylist_({
|
|
mediaGroups: {},
|
|
playlists: []
|
|
}, {
|
|
attributes: {
|
|
CODECS: 'mp4a.40.E,avc1.deadbeef'
|
|
}
|
|
}), [
|
|
'video/mp2t; codecs="avc1.deadbeef, mp4a.40.E"'
|
|
], 'returned the parsed muxed type');
|
|
});
|
|
|
|
QUnit.test('recognizes mixed codec configurations', function(assert) {
|
|
assert.deepEqual(mimeTypesForPlaylist_({
|
|
mediaGroups: {
|
|
AUDIO: {
|
|
hi: {
|
|
en: {},
|
|
es: {
|
|
uri: 'http://example.com/alt-audio.m3u8'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
playlists: []
|
|
}, {
|
|
attributes: {
|
|
AUDIO: 'hi'
|
|
}
|
|
}), [
|
|
'video/mp2t; codecs="avc1.4d400d, mp4a.40.2"',
|
|
'audio/mp2t; codecs="mp4a.40.2"'
|
|
], 'returned a default muxed type with alternate audio');
|
|
|
|
assert.deepEqual(mimeTypesForPlaylist_({
|
|
mediaGroups: {
|
|
AUDIO: {
|
|
hi: {
|
|
eng: {},
|
|
es: {
|
|
uri: 'http://example.com/alt-audio.m3u8'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
playlists: []
|
|
}, {
|
|
attributes: {
|
|
CODECS: 'mp4a.40.E,avc1.deadbeef',
|
|
AUDIO: 'hi'
|
|
}
|
|
}), [
|
|
'video/mp2t; codecs="avc1.deadbeef, mp4a.40.E"',
|
|
'audio/mp2t; codecs="mp4a.40.E"'
|
|
], 'returned a parsed muxed type with alternate audio');
|
|
});
|
|
|
|
QUnit.test('recognizes unmuxed codec configurations', function(assert) {
|
|
assert.deepEqual(mimeTypesForPlaylist_({
|
|
mediaGroups: {
|
|
AUDIO: {
|
|
hi: {
|
|
eng: {
|
|
uri: 'http://example.com/eng.m3u8'
|
|
},
|
|
es: {
|
|
uri: 'http://example.com/eng.m3u8'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
playlists: []
|
|
}, {
|
|
attributes: {
|
|
AUDIO: 'hi'
|
|
}
|
|
}), [
|
|
'video/mp2t; codecs="avc1.4d400d"',
|
|
'audio/mp2t; codecs="mp4a.40.2"'
|
|
], 'returned default unmuxed types');
|
|
|
|
assert.deepEqual(mimeTypesForPlaylist_({
|
|
mediaGroups: {
|
|
AUDIO: {
|
|
hi: {
|
|
eng: {
|
|
uri: 'http://example.com/alt-audio.m3u8'
|
|
},
|
|
es: {
|
|
uri: 'http://example.com/eng.m3u8'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
playlists: []
|
|
}, {
|
|
attributes: {
|
|
CODECS: 'mp4a.40.E,avc1.deadbeef',
|
|
AUDIO: 'hi'
|
|
}
|
|
}), [
|
|
'video/mp2t; codecs="avc1.deadbeef"',
|
|
'audio/mp2t; codecs="mp4a.40.E"'
|
|
], 'returned parsed unmuxed types');
|
|
});
|