videojs-contrib-hls
Version:
Play back HLS with video.js, even where it's not natively supported
1,569 lines (1,320 loc) • 85.4 kB
JavaScript
/* eslint-disable max-len */
import document from 'global/document';
import videojs from 'video.js';
import Events from 'video.js';
import QUnit from 'qunit';
import testDataManifests from './test-manifests.js';
import {
useFakeEnvironment,
useFakeMediaSource,
createPlayer,
openMediaSource,
standardXHRResponse,
absoluteUrl
} from './test-helpers.js';
/* eslint-disable no-unused-vars */
// we need this so that it can register hls with videojs
import {HlsSourceHandler, HlsHandler, Hls} from '../src/videojs-contrib-hls';
import HlsAudioTrack from '../src/hls-audio-track';
import window from 'global/window';
/* eslint-enable no-unused-vars */
const Flash = videojs.getComponent('Flash');
let nextId = 0;
// do a shallow copy of the properties of source onto the target object
const merge = function(target, source) {
let name;
for (name in source) {
target[name] = source[name];
}
};
QUnit.module('HLS', {
beforeEach() {
this.env = useFakeEnvironment();
this.requests = this.env.requests;
this.mse = useFakeMediaSource();
this.clock = this.env.clock;
this.old = {};
// mock out Flash features for phantomjs
this.old.Flash = videojs.mergeOptions({}, Flash);
/* eslint-disable camelcase */
Flash.embed = function(swf, flashVars) {
let el = document.createElement('div');
el.id = 'vjs_mock_flash_' + nextId++;
el.className = 'vjs-tech vjs-mock-flash';
el.duration = Infinity;
el.vjs_load = function() {};
el.vjs_getProperty = function(attr) {
if (attr === 'buffered') {
return [[0, 0]];
}
return el[attr];
};
el.vjs_setProperty = function(attr, value) {
el[attr] = value;
};
el.vjs_src = function() {};
el.vjs_play = function() {};
el.vjs_discontinuity = function() {};
if (flashVars.autoplay) {
el.autoplay = true;
}
if (flashVars.preload) {
el.preload = flashVars.preload;
}
el.currentTime = 0;
return el;
};
/* eslint-enable camelcase */
this.old.FlashSupported = Flash.isSupported;
Flash.isSupported = function() {
return true;
};
// store functionality that some tests need to mock
this.old.GlobalOptions = videojs.mergeOptions(videojs.options);
// force the HLS tech to run
this.old.NativeHlsSupport = videojs.Hls.supportsNativeHls;
videojs.Hls.supportsNativeHls = false;
this.old.Decrypt = videojs.Hls.Decrypter;
videojs.Hls.Decrypter = function() {};
// save and restore browser detection for the Firefox-specific tests
this.old.IS_FIREFOX = videojs.browser.IS_FIREFOX;
// setup a player
this.player = createPlayer();
},
afterEach() {
this.env.restore();
this.mse.restore();
merge(videojs.options, this.old.GlobalOptions);
Flash.isSupported = this.old.FlashSupported;
merge(Flash, this.old.Flash);
videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport;
videojs.Hls.Decrypter = this.old.Decrypt;
videojs.browser.IS_FIREFOX = this.old.IS_FIREFOX;
this.player.dispose();
}
});
QUnit.test('deprication warning is show when using player.hls', function() {
let oldWarn = videojs.log.warn;
let warning = '';
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
videojs.log.warn = (text) => {
warning = text;
};
let hls = this.player.hls;
QUnit.equal(warning, 'player.hls is deprecated. Use player.tech_.hls instead.', 'warning would have been shown');
QUnit.ok(hls, 'an instance of hls is returned by player.hls');
videojs.log.warn = oldWarn;
});
QUnit.test('starts playing if autoplay is specified', function() {
this.player.autoplay(true);
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
// make sure play() is called *after* the media source opens
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
QUnit.ok(!this.player.paused(), 'not paused');
});
QUnit.test('stats are reset on each new source', function() {
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
// make sure play() is called *after* the media source opens
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, 'stat is set');
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 0, 'stat is reset');
});
QUnit.test('XHR requests first byte range on play', function() {
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.triggerReady();
this.clock.tick(1);
this.player.tech_.trigger('play');
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
QUnit.equal(this.requests[1].headers.Range, 'bytes=0-522827');
});
QUnit.test('Seeking requests correct byte range', function() {
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.trigger('play');
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.clock.tick(1);
this.player.currentTime(40);
this.clock.tick(1);
QUnit.equal(this.requests[2].headers.Range, 'bytes=2299992-2835603');
});
QUnit.test('autoplay seeks to the live point after playlist load', function() {
let currentTime = 0;
this.player.autoplay(true);
this.player.on('seeking', () => {
currentTime = this.player.currentTime();
});
this.player.src({
src: 'liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
standardXHRResponse(this.requests.shift());
this.clock.tick(1);
QUnit.notEqual(currentTime, 0, 'seeked on autoplay');
});
QUnit.test('autoplay seeks to the live point after media source open', function() {
let currentTime = 0;
this.player.autoplay(true);
this.player.on('seeking', () => {
currentTime = this.player.currentTime();
});
this.player.src({
src: 'liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
this.clock.tick(1);
QUnit.notEqual(currentTime, 0, 'seeked on autoplay');
});
QUnit.test('duration is set when the source opens after the playlist is loaded', function() {
this.player.src({
src: 'media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
QUnit.equal(this.player.tech_.hls.mediaSource.duration,
40,
'set the duration');
});
QUnit.test('codecs are passed to the source buffer', function() {
let codecs = [];
this.player.src({
src: 'custom-codecs.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
let addSourceBuffer = this.player.tech_.hls.mediaSource.addSourceBuffer;
this.player.tech_.hls.mediaSource.addSourceBuffer = function(codec) {
codecs.push(codec);
return addSourceBuffer.call(this, codec);
};
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:CODECS="avc1.dd00dd, mp4a.40.f"\n' +
'media.m3u8\n');
standardXHRResponse(this.requests.shift());
QUnit.equal(codecs.length, 1, 'created a source buffer');
QUnit.equal(codecs[0], 'video/mp2t; codecs="avc1.dd00dd, mp4a.40.f"', 'specified the codecs');
});
QUnit.test('including HLS as a tech does not error', function() {
let player = createPlayer({
techOrder: ['hls', 'html5']
});
QUnit.ok(player, 'created the player');
QUnit.equal(this.env.log.warn.calls, 2, 'logged two warnings for deprications');
});
QUnit.test('creates a PlaylistLoader on init', function() {
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.src({
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
QUnit.equal(this.requests[0].aborted, true, 'aborted previous src');
standardXHRResponse(this.requests[1]);
QUnit.ok(this.player.tech_.hls.playlists.master,
'set the master playlist');
QUnit.ok(this.player.tech_.hls.playlists.media(),
'set the media playlist');
QUnit.ok(this.player.tech_.hls.playlists.media().segments,
'the segment entries are parsed');
QUnit.strictEqual(this.player.tech_.hls.playlists.master.playlists[0],
this.player.tech_.hls.playlists.media(),
'the playlist is selected');
});
QUnit.test('sets the duration if one is available on the playlist', function() {
let events = 0;
this.player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.on('durationchange', function() {
events++;
});
standardXHRResponse(this.requests[0]);
QUnit.equal(this.player.tech_.hls.mediaSource.duration,
40,
'set the duration');
QUnit.equal(events, 1, 'durationchange is fired');
});
QUnit.test('estimates individual segment durations if needed', function() {
let changes = 0;
this.player.src({
src: 'http://example.com/manifest/missingExtinf.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.mediaSource.duration = NaN;
this.player.tech_.on('durationchange', function() {
changes++;
});
standardXHRResponse(this.requests[0]);
QUnit.strictEqual(this.player.tech_.hls.mediaSource.duration,
this.player.tech_.hls.playlists.media().segments.length * 10,
'duration is updated');
QUnit.strictEqual(changes, 1, 'one durationchange fired');
});
QUnit.test('translates seekable by the starting time for live playlists', function() {
let seekable;
this.player.src({
src: 'media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:15\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
seekable = this.player.seekable();
QUnit.equal(seekable.length, 1, 'one seekable range');
QUnit.equal(seekable.start(0), 0, 'the earliest possible position is at zero');
QUnit.equal(seekable.end(0), 10, 'end is relative to the start');
});
QUnit.test('starts downloading a segment on loadedmetadata', function() {
this.player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.buffered = function() {
return videojs.createTimeRange(0, 0);
};
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
QUnit.strictEqual(this.requests[1].url,
absoluteUrl('manifest/media-00001.ts'),
'the first segment is requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('re-initializes the handler for each source', function() {
let firstPlaylists;
let secondPlaylists;
let firstMSE;
let secondMSE;
let aborts = 0;
let masterPlaylistController;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
firstPlaylists = this.player.tech_.hls.playlists;
firstMSE = this.player.tech_.hls.mediaSource;
standardXHRResponse(this.requests.shift());
standardXHRResponse(this.requests.shift());
masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
masterPlaylistController.mainSegmentLoader_.sourceUpdater_.sourceBuffer_.abort = () => {
aborts++;
};
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
secondPlaylists = this.player.tech_.hls.playlists;
secondMSE = this.player.tech_.hls.mediaSource;
QUnit.equal(1, aborts, 'aborted the old source buffer');
QUnit.ok(this.requests[0].aborted, 'aborted the old segment request');
QUnit.notStrictEqual(firstPlaylists,
secondPlaylists,
'the playlist object is not reused');
QUnit.notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused');
});
QUnit.test('triggers an error when a master playlist request errors', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests.pop().respond(500);
QUnit.equal(this.player.tech_.hls.mediaSource.error_,
'network',
'a network error is triggered');
});
QUnit.test('downloads media playlists after loading the master', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 20e10;
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
standardXHRResponse(this.requests[2]);
QUnit.strictEqual(this.requests[0].url,
'manifest/master.m3u8',
'master playlist requested');
QUnit.strictEqual(this.requests[1].url,
absoluteUrl('manifest/media2.m3u8'),
'media playlist requested');
QUnit.strictEqual(this.requests[2].url,
absoluteUrl('manifest/media2-00001.ts'),
'first segment requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('upshifts if the initial bandwidth hint is high', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 10e20;
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
standardXHRResponse(this.requests[2]);
QUnit.strictEqual(
this.requests[0].url,
'manifest/master.m3u8',
'master playlist requested'
);
QUnit.strictEqual(
this.requests[1].url,
absoluteUrl('manifest/media2.m3u8'),
'media playlist requested'
);
QUnit.strictEqual(
this.requests[2].url,
absoluteUrl('manifest/media2-00001.ts'),
'first segment requested'
);
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('downshifts if the initial bandwidth hint is low', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 100;
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
standardXHRResponse(this.requests[2]);
QUnit.strictEqual(this.requests[0].url,
'manifest/master.m3u8',
'master playlist requested');
QUnit.strictEqual(this.requests[1].url,
absoluteUrl('manifest/media1.m3u8'),
'media playlist requested');
QUnit.strictEqual(this.requests[2].url,
absoluteUrl('manifest/media1-00001.ts'),
'first segment requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('buffer checks are noops until a media playlist is ready', function() {
this.player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.clock.tick(10 * 1000);
QUnit.strictEqual(1, this.requests.length, 'one request was made');
QUnit.strictEqual(this.requests[0].url,
'manifest/media.m3u8',
'media playlist requested');
});
QUnit.test('buffer checks are noops when only the master is ready', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
// master
standardXHRResponse(this.requests.shift());
// media
standardXHRResponse(this.requests.shift());
// ignore any outstanding segment requests
this.requests.length = 0;
// load in a new playlist which will cause playlists.media() to be
// undefined while it is being fetched
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
// respond with the master playlist but don't send the media playlist yet
// force media1 to be requested
this.player.tech_.hls.bandwidth = 1;
// master
standardXHRResponse(this.requests.shift());
this.clock.tick(10 * 1000);
QUnit.strictEqual(1, this.requests.length, 'one request was made');
QUnit.strictEqual(this.requests[0].url,
absoluteUrl('manifest/media1.m3u8'),
'media playlist requested');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('selects a playlist below the current bandwidth', function() {
let playlist;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
// the default playlist has a really high bitrate
this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
// playlist 1 has a very low bitrate
this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1;
// but the detected client bandwidth is really low
this.player.tech_.hls.bandwidth = 10;
playlist = this.player.tech_.hls.selectPlaylist();
QUnit.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[1],
'the low bitrate stream is selected');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 10, 'bandwidth set above');
});
QUnit.test('allows initial bandwidth to be provided', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 500;
this.requests[0].bandwidth = 1;
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
'#EXT-X-TARGETDURATION:10\n');
QUnit.equal(this.player.tech_.hls.bandwidth,
500,
'prefers user-specified initial bandwidth');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 500, 'bandwidth set above');
});
QUnit.test('raises the minimum bitrate for a stream proportionially', function() {
let playlist;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
// the default playlist's bandwidth + 10% is QUnit.equal to the current bandwidth
this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
this.player.tech_.hls.bandwidth = 11;
// 9.9 * 1.1 < 11
this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9;
playlist = this.player.tech_.hls.selectPlaylist();
QUnit.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[1],
'a lower bitrate stream is selected');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 11, 'bandwidth set above');
});
QUnit.test('uses the lowest bitrate if no other is suitable', function() {
let playlist;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
// the lowest bitrate playlist is much greater than 1b/s
this.player.tech_.hls.bandwidth = 1;
playlist = this.player.tech_.hls.selectPlaylist();
// playlist 1 has the lowest advertised bitrate
QUnit.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[1],
'the lowest bitrate stream is selected');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('selects the correct rendition by tech dimensions', function() {
let playlist;
let hls;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
hls = this.player.tech_.hls;
this.player.width(640);
this.player.height(360);
hls.bandwidth = 3000000;
playlist = hls.selectPlaylist();
QUnit.deepEqual(playlist.attributes.RESOLUTION,
{width: 960, height: 540},
'should return the correct resolution by tech dimensions');
QUnit.equal(playlist.attributes.BANDWIDTH,
1928000,
'should have the expected bandwidth in case of multiple');
this.player.width(1920);
this.player.height(1080);
hls.bandwidth = 3000000;
playlist = hls.selectPlaylist();
QUnit.deepEqual(playlist.attributes.RESOLUTION,
{width: 960, height: 540},
'should return the correct resolution by tech dimensions');
QUnit.equal(playlist.attributes.BANDWIDTH,
1928000,
'should have the expected bandwidth in case of multiple');
this.player.width(396);
this.player.height(224);
playlist = hls.selectPlaylist();
QUnit.deepEqual(playlist.attributes.RESOLUTION,
{width: 396, height: 224},
'should return the correct resolution by ' +
'tech dimensions, if exact match');
QUnit.equal(playlist.attributes.BANDWIDTH,
440000,
'should have the expected bandwidth in case of multiple, if exact match');
this.player.width(395);
this.player.height(222);
playlist = this.player.tech_.hls.selectPlaylist();
QUnit.deepEqual(playlist.attributes.RESOLUTION,
{width: 396, height: 224},
'should return the next larger resolution by tech dimensions, ' +
'if no exact match exists');
QUnit.equal(playlist.attributes.BANDWIDTH,
440000,
'should have the expected bandwidth in case of multiple, if exact match');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 3000000, 'bandwidth set above');
});
QUnit.test('selects the highest bitrate playlist when the player dimensions are ' +
'larger than any of the variants', function() {
let playlist;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
// master
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1000,RESOLUTION=2x1\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1,RESOLUTION=1x1\n' +
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
this.player.tech_.hls.bandwidth = 1e10;
this.player.width(1024);
this.player.height(768);
playlist = this.player.tech_.hls.selectPlaylist();
QUnit.equal(playlist.attributes.BANDWIDTH,
1000,
'selected the highest bandwidth variant');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
});
QUnit.test('filters playlists that are currently excluded', function() {
let playlist;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
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=1000\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
// exclude the current playlist
this.player.tech_.hls.playlists.master.playlists[0].excludeUntil = +new Date() + 1000;
playlist = this.player.tech_.hls.selectPlaylist();
QUnit.equal(playlist,
this.player.tech_.hls.playlists.master.playlists[1],
'respected exclusions');
// timeout the exclusion
this.clock.tick(1000);
playlist = this.player.tech_.hls.selectPlaylist();
QUnit.equal(playlist,
this.player.tech_.hls.playlists.master.playlists[0],
'expired the exclusion');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
});
QUnit.test('does not blacklist compatible H.264 codec strings', function() {
let master;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
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.4d400f,mp4a.40.5"\n' +
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
master = this.player.tech_.hls.playlists.master;
QUnit.strictEqual(typeof master.playlists[0].excludeUntil,
'undefined',
'did not blacklist');
QUnit.strictEqual(typeof master.playlists[1].excludeUntil,
'undefined',
'did not blacklist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('does not blacklist compatible AAC codec strings', function() {
let master;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
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.2"\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,not-an-audio-codec"\n' +
'media1.m3u8\n');
// media
standardXHRResponse(this.requests.shift());
master = this.player.tech_.hls.playlists.master;
QUnit.strictEqual(typeof master.playlists[0].excludeUntil,
'undefined',
'did not blacklist mp4a.40.2');
QUnit.strictEqual(master.playlists[1].excludeUntil,
Infinity,
'blacklisted invalid audio codec');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
});
QUnit.test('cancels outstanding XHRs when seeking', function() {
this.player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.player.tech_.hls.media = {
segments: [{
uri: '0.ts',
duration: 10
}, {
uri: '1.ts',
duration: 10
}]
};
// attempt to seek while the download is in progress
this.player.currentTime(7);
this.clock.tick(1);
QUnit.ok(this.requests[1].aborted, 'XHR aborted');
QUnit.strictEqual(this.requests.length, 3, 'opened new XHR');
});
QUnit.test('does not abort segment loading for in-buffer seeking', function() {
this.player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
this.player.tech_.buffered = function() {
return videojs.createTimeRange(0, 20);
};
this.player.tech_.setCurrentTime(11);
this.clock.tick(1);
QUnit.equal(this.requests.length, 1, 'did not abort the outstanding request');
});
QUnit.test('playlist 404 should end stream with a network error', function() {
this.player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests.pop().respond(404);
QUnit.equal(this.player.tech_.hls.mediaSource.error_, 'network', 'set a network error');
});
QUnit.test('segment 404 should trigger blacklisting of media', function() {
let media;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 20000;
// master
standardXHRResponse(this.requests[0]);
// media
standardXHRResponse(this.requests[1]);
media = this.player.tech_.hls.playlists.media_;
// segment
this.requests[2].respond(400);
QUnit.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
QUnit.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
});
QUnit.test('playlist 404 should blacklist media', function() {
let media;
let url;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 1e10;
// master
this.requests[0].respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media1.m3u8\n');
QUnit.equal(typeof this.player.tech_.hls.playlists.media_,
'undefined',
'no media is initially set');
// media
this.requests[1].respond(400);
url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
media = this.player.tech_.hls.playlists.master.playlists[url];
QUnit.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
QUnit.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
});
QUnit.test('seeking in an empty playlist is a non-erroring noop', function() {
let requestsLength;
this.player.src({
src: 'manifest/empty-live.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests.shift().respond(200, null, '#EXTM3U\n');
requestsLength = this.requests.length;
this.player.tech_.setCurrentTime(183);
this.clock.tick(1);
QUnit.equal(this.requests.length, requestsLength, 'made no additional requests');
});
QUnit.test('fire loadedmetadata once we successfully load a playlist', function() {
let count = 0;
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
let hls = this.player.tech_.hls;
hls.bandwidth = 20000;
hls.masterPlaylistController_.masterPlaylistLoader_.on('loadedmetadata', function() {
count += 1;
});
// master
standardXHRResponse(this.requests.shift());
QUnit.equal(count, 0,
'loadedMedia not triggered before requesting playlist');
// media
this.requests.shift().respond(404);
QUnit.equal(count, 0,
'loadedMedia not triggered after playlist 404');
QUnit.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
// media
standardXHRResponse(this.requests.shift());
QUnit.equal(count, 1,
'loadedMedia triggered after successful recovery from 404');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
});
QUnit.test('sets seekable and duration for live playlists', function() {
this.player.src({
src: 'http://example.com/manifest/missingEndlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
QUnit.equal(this.player.tech_.hls.mediaSource.seekable.length,
1,
'set one seekable range');
QUnit.equal(this.player.tech_.hls.mediaSource.seekable.start(0),
this.player.tech_.hls.seekable().start(0),
'set seekable start');
QUnit.equal(this.player.tech_.hls.mediaSource.seekable.end(0),
this.player.tech_.hls.seekable().end(0),
'set seekable end');
QUnit.strictEqual(this.player.tech_.hls.mediaSource.duration,
Infinity,
'duration on the mediaSource is infinity');
});
QUnit.test('live playlist starts with correct currentTime value', function() {
this.player.src({
src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);
this.player.tech_.hls.playlists.trigger('loadedmetadata');
this.player.tech_.paused = function() {
return false;
};
this.player.tech_.trigger('play');
this.clock.tick(1);
let media = this.player.tech_.hls.playlists.media();
QUnit.strictEqual(this.player.currentTime(),
Hls.Playlist.seekable(media).end(0),
'currentTime is updated at playback');
});
QUnit.test('adjusts the seekable start based on the amount of expired live content', function() {
this.player.src({
src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
// add timeline info to the playlist
this.player.tech_.hls.playlists.media().segments[1].end = 29.5;
// expired_ should be ignored if there is timeline information on
// the playlist
this.player.tech_.hls.playlists.expired_ = 172;
QUnit.equal(this.player.seekable().start(0),
29.5 - 29,
'offset the seekable start');
});
QUnit.test('estimates seekable ranges for live streams that have been paused for a long time', function() {
this.player.src({
src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
this.player.tech_.hls.playlists.expired_ = 172;
QUnit.equal(this.player.seekable().start(0),
this.player.tech_.hls.playlists.expired_,
'offset the seekable start');
});
QUnit.test('resets the time to a seekable position when resuming a live stream ' +
'after a long break', function() {
let seekTarget;
this.player.src({
src: 'live0.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:16\n' +
'#EXTINF:10,\n' +
'16.ts\n');
// mock out the player to simulate a live stream that has been
// playing for awhile
this.player.tech_.hls.seekable = function() {
return videojs.createTimeRange(160, 170);
};
this.player.tech_.setCurrentTime = function(time) {
if (typeof time !== 'undefined') {
seekTarget = time;
}
};
this.player.tech_.played = function() {
return videojs.createTimeRange(120, 170);
};
this.player.tech_.trigger('playing');
this.player.tech_.trigger('play');
QUnit.equal(seekTarget,
this.player.seekable().start(0),
'seeked to the start of seekable');
this.player.tech_.trigger('seeked');
});
QUnit.test('reloads out-of-date live playlists when switching variants', function() {
let oldManifest = testDataManifests['variant-update'];
this.player.src({
src: 'http://example.com/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.master = {
playlists: [{
mediaSequence: 15,
segments: [1, 1, 1]
}, {
uri: 'http://example.com/variant-update.m3u8',
mediaSequence: 0,
segments: [1, 1]
}]
};
// playing segment 15 on playlist zero
this.player.tech_.hls.media = this.player.tech_.hls.master.playlists[0];
this.player.mediaIndex = 1;
testDataManifests['variant-update'] = '#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:16\n' +
'#EXTINF:10,\n' +
'16.ts\n' +
'#EXTINF:10,\n' +
'17.ts\n';
// switch playlists
this.player.tech_.hls.selectPlaylist = function() {
return this.player.tech_.hls.master.playlists[1];
};
// timeupdate downloads segment 16 then switches playlists
this.player.trigger('timeupdate');
QUnit.strictEqual(this.player.mediaIndex, 1, 'mediaIndex points at the next segment');
testDataManifests['variant-update'] = oldManifest;
});
QUnit.test('if withCredentials global option is used, withCredentials is set on the XHR object', function() {
let hlsOptions = videojs.options.hls;
this.player.dispose();
videojs.options.hls = {
withCredentials: true
};
this.player = createPlayer();
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
QUnit.ok(this.requests[0].withCredentials,
'with credentials should be set to true if that option is passed in');
videojs.options.hls = hlsOptions;
});
QUnit.test('the withCredentials option overrides the global default', function() {
let hlsOptions = videojs.options.hls;
this.player.dispose();
videojs.options.hls = {
withCredentials: true
};
this.player = createPlayer();
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl',
withCredentials: false
});
openMediaSource(this.player, this.clock);
QUnit.ok(!this.requests[0].withCredentials,
'with credentials should be set to false if if overrode global option');
videojs.options.hls = hlsOptions;
});
QUnit.test('if mode global option is used, mode is set to global option', function() {
let hlsOptions = videojs.options.hls;
this.player.dispose();
videojs.options.hls = {
mode: 'flash'
};
this.player = createPlayer();
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
QUnit.equal(this.player.tech_.hls.options_.mode, 'flash', 'mode set to flash');
videojs.options.hls = hlsOptions;
});
QUnit.test('respects bandwidth option of 0', function() {
this.player.dispose();
this.player = createPlayer({ html5: { hls: { bandwidth: 0 } } });
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
QUnit.equal(this.player.tech_.hls.bandwidth, 0, 'set bandwidth to 0');
});
QUnit.test('uses default bandwidth option if non-numerical value provided', function() {
this.player.dispose();
this.player = createPlayer({ html5: { hls: { bandwidth: 'garbage' } } });
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
QUnit.equal(this.player.tech_.hls.bandwidth, 4194304, 'set bandwidth to default');
});
QUnit.test('does not break if the playlist has no segments', function() {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
try {
openMediaSource(this.player, this.clock);
this.requests[0].respond(200, null,
'#EXTM3U\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
'#EXT-X-TARGETDURATION:10\n');
} catch (e) {
QUnit.ok(false, 'an error was thrown');
throw e;
}
QUnit.ok(true, 'no error was thrown');
QUnit.strictEqual(
this.requests.length,
1,
'no this.requestsfor non-existent segments were queued'
);
});
QUnit.test('can seek before the source buffer opens', function() {
this.player.src({
src: 'media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
this.player.triggerReady();
this.player.currentTime(1);
QUnit.equal(this.player.currentTime(), 1, 'seeked');
});
QUnit.test('resets the switching algorithm if a request times out', function() {
this.player.src({
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.hls.bandwidth = 1e20;
// master
standardXHRResponse(this.requests.shift());
// media.m3u8
standardXHRResponse(this.requests.shift());
// simulate a segment timeout
this.requests[0].timedout = true;
// segment
this.requests.shift().abort();
standardXHRResponse(this.requests.shift());
QUnit.strictEqual(this.player.tech_.hls.playlists.media(),
this.player.tech_.hls.playlists.master.playlists[1],
'reset to the lowest bitrate playlist');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth is reset too');
});
QUnit.test('disposes the playlist loader', function() {
let disposes = 0;
let player;
let loaderDispose;
player = createPlayer();
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player, this.clock);
loaderDispose = player.tech_.hls.playlists.dispose;
player.tech_.hls.playlists.dispose = function() {
disposes++;
loaderDispose.call(player.tech_.hls.playlists);
};
player.dispose();
QUnit.strictEqual(disposes, 1, 'disposed playlist loader');
});
QUnit.test('remove event handlers on dispose', function() {
let player;
let unscoped = 0;
player = createPlayer();
player.on = function(owner) {
if (typeof owner !== 'object') {
unscoped++;
}
};
player.off = function(owner) {
if (typeof owner !== 'object') {
unscoped--;
}
};
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player, this.clock);
standardXHRResponse(this.requests[0]);
standardXHRResponse(this.requests[1]);
player.dispose();
QUnit.ok(unscoped <= 0, 'no unscoped handlers');
});
QUnit.test('the source handler supports HLS mime types', function() {
['html5', 'flash'].forEach(function(techName) {
QUnit.ok(HlsSourceHandler(techName).canHandleSource({
type: 'aPplicatiOn/x-MPegUrl'
}), 'supports x-mpegurl');
QUnit.ok(HlsSourceHandler(techName).canHandleSource({
type: 'aPplicatiOn/VnD.aPPle.MpEgUrL'
}), 'supports vnd.apple.mpegurl');
QUnit.ok(HlsSourceHandler(techName).canPlayType('aPplicatiOn/VnD.aPPle.MpEgUrL'),
'supports vnd.apple.mpegurl');
QUnit.ok(HlsSourceHandler(techName).canPlayType('aPplicatiOn/x-MPegUrl'),
'supports x-mpegurl');
QUnit.ok(!(HlsSourceHandler(techName).canHandleSource({
type: 'video/mp4'
}) instanceof HlsHandler), 'does not support mp4');
QUnit.ok(!(HlsSourceHandler(techName).canHandleSource({
type: 'video/x-flv'
}) instanceof HlsHandler), 'does not support flv');
QUnit.ok(!(HlsSourceHandler(techName).canPlayType('video/mp4')),
'does not support mp4');
QUnit.ok(!(HlsSourceHandler(techName).canPlayType('video/x-flv')),
'does not support flv');
});
});
QUnit.test('fires loadstart manually if Flash is used', function() {
let tech = new (videojs.getTech('Flash'))({});
let loadstarts = 0;
tech.on('loadstart', function() {
loadstarts++;
});
HlsSourceHandler('flash').handleSource({
src: 'movie.m3u8',
type: 'application/x-mpegURL'
}, tech);
QUnit.equal(loadstarts, 0, 'loadstart is not synchronous');
this.clock.tick(1);
QUnit.equal(loadstarts, 1, 'fired loadstart');
});
QUnit.test('has no effect if native HLS is available', function() {
let player;
Hls.supportsNativeHls = true;
player = createPlayer();
player.src({
src: 'http://example.com/manifest/master.m3u8',
type: 'application/x-mpegURL'
});
QUnit.ok(!player.tech_.hls, 'did not load hls tech');
player.dispose();
});
// TODO: this test seems to be very old do we still need it?
// it does not appear to test anything at all...
QUnit.skip('is not supported on browsers without typed arrays', function() {
let oldArray = window.Uint8Array;
window.Uint8Array = null;
QUnit.ok(!videojs.Hls.isSupported(), 'HLS is not supported');
// cleanup
window.Uint8Array = oldArray;
});
QUnit.test('re-emits mediachange events', function() {
let mediaChanges = 0;
this.player.on('mediachange', function() {
mediaChanges++;
});
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests.shift());
this.player.tech_.hls.playlists.trigger('mediachange');
QUnit.strictEqual(mediaChanges, 1, 'fired mediachange');
});
QUnit.test('can be disposed before finishing initialization', function() {
let readyHandlers = [];
this.player.ready = function(callback) {
readyHandlers.push(callback);
};
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
this.player.src({
src: 'http://example.com/media.mp4',
type: 'video/mp4'
});
QUnit.ok(readyHandlers.length > 0, 'registered a ready handler');
try {
while (readyHandlers.length) {
readyHandlers.shift().call(this.player);
openMediaSource(this.player, this.clock);
}
QUnit.ok(true, 'did not throw an exception');
} catch (e) {
QUnit.ok(false, 'threw an exception');
}
});
QUnit.test('calling play() at the end of a video replays', function() {
let seekTime = -1;
this.player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.setCurrentTime = function(time) {
if (typeof time !== 'undefined') {
seekTime = time;
}
return 0;
};
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(this.requests.shift());
this.player.tech_.ended = function() {
return true;
};
this.player.tech_.trigger('play');
QUnit.equal(seekTime, 0, 'seeked to the beginning');
// verify stats
QUnit.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 16, '16 bytes');
QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
});
QUnit.test('keys are resolved relative to the master playlist', function() {
this.player.src({
src: 'video/master-encrypted.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
'playlist/playlist.m3u8\n' +
'#EXT-X-ENDLIST\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-TARGETDURATION:15\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence1.ts\n' +
'#EXT-X-ENDLIST\n');
QUnit.equal(this.requests.length, 2, 'requested the key');
QUnit.equal(this.requests[0].