12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462 |
- 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_,
- mapLegacyAvcCodecs_
- } 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';
- import Config from '../src/config';
- const generateMedia = function(isMaat, isMuxed, hasVideoCodec, hasAudioCodec, isFMP4) {
- const codec = (hasVideoCodec ? 'avc1.deadbeef' : '') +
- (hasVideoCodec && hasAudioCodec ? ',' : '') +
- (hasAudioCodec ? 'mp4a.40.E' : '');
- const master = {
- mediaGroups: {},
- playlists: []
- };
- const media = {
- attributes: {}
- };
- if (isMaat) {
- master.mediaGroups.AUDIO = {
- test: {
- demuxed: {
- uri: 'foo.bar'
- }
- }
- };
- if (isMuxed) {
- master.mediaGroups.AUDIO.test.muxed = {};
- }
- media.attributes.AUDIO = 'test';
- }
- if (isFMP4) {
- // This is not a great way to signal that the playlist is fmp4 but
- // this is how we currently detect it in HLS so let's emulate it here
- media.segments = [
- {
- map: 'test'
- }
- ];
- }
- if (hasVideoCodec || hasAudioCodec) {
- media.attributes.CODECS = codec;
- }
- return [master, media];
- };
- 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.oldBrowser = videojs.browser;
- videojs.browser = videojs.mergeOptions({}, videojs.browser);
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- this.standardXHRResponse = (request, data) => {
- standardXHRResponse(request, data);
- // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
- // we have to use clock.tick to get the expected side effects of
- // SegmentLoader#handleUpdateEnd_
- this.clock.tick(1);
- };
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // Make segment metadata noop since most test segments dont have real data
- this.masterPlaylistController.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
- },
- afterEach() {
- this.env.restore();
- this.mse.restore();
- videojs.Hls.supportsNativeHls = this.origSupportsNativeHls;
- videojs.browser = this.oldBrowser;
- 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
- this.standardXHRResponse(this.requests.shift());
- // playlist
- this.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('passes options to PlaylistLoader', function(assert) {
- const options = {
- url: 'test',
- tech: this.player.tech_
- };
- let controller = new MasterPlaylistController(options);
- assert.notOk(controller.masterPlaylistLoader_.withCredentials, 'credentials wont be sent by default');
- assert.notOk(controller.masterPlaylistLoader_.handleManifestRedirects, 'redirects are ignored by default');
- controller = new MasterPlaylistController(Object.assign({
- withCredentials: true,
- handleManifestRedirects: true
- }, options));
- assert.ok(controller.masterPlaylistLoader_.withCredentials, 'withCredentials enabled');
- assert.ok(controller.masterPlaylistLoader_.handleManifestRedirects, 'handleManifestRedirects enabled');
- });
- QUnit.test('obeys auto preload option', function(assert) {
- this.player.preload('auto');
- // master
- this.standardXHRResponse(this.requests.shift());
- // playlist
- this.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
- this.standardXHRResponse(this.requests.shift());
- // playlist
- this.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('resets SegmentLoader when seeking in flash for both in and out of buffer',
- function(assert) {
- let resets = 0;
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- let mpc = this.masterPlaylistController;
- let segmentLoader = mpc.mainSegmentLoader_;
- segmentLoader.resetEverything = function() {
- resets++;
- };
- let buffered;
- mpc.tech_.buffered = function() {
- return buffered;
- };
- buffered = videojs.createTimeRanges([[0, 20]]);
- mpc.mode_ = 'html5';
- mpc.setCurrentTime(10);
- assert.equal(resets, 0,
- 'does not reset loader when seeking into a buffered region in html5');
- mpc.setCurrentTime(21);
- assert.equal(resets, 1,
- 'does reset loader when seeking outside of the buffered region in html5');
- mpc.mode_ = 'flash';
- mpc.setCurrentTime(10);
- assert.equal(resets, 2,
- 'does reset loader when seeking into a buffered region in flash');
- mpc.setCurrentTime(21);
- assert.equal(resets, 3,
- 'does reset loader when seeking outside of the buffered region in flash');
- });
- QUnit.test('selects lowest bitrate rendition when enableLowInitialPlaylist is set',
- function(assert) {
- // Set requests.length to 0, otherwise it will use the requests generated in the
- // beforeEach function
- this.requests.length = 0;
- this.player = createPlayer({ html5: { hls: { enableLowInitialPlaylist: true } } });
- this.player.src({
- src: 'manifest/master.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- let numCallsToSelectInitialPlaylistCalls = 0;
- let numCallsToSelectPlaylist = 0;
- this.masterPlaylistController.selectPlaylist = () => {
- numCallsToSelectPlaylist++;
- return this.masterPlaylistController.master().playlists[0];
- };
- this.masterPlaylistController.selectInitialPlaylist = () => {
- numCallsToSelectInitialPlaylistCalls++;
- return this.masterPlaylistController.master().playlists[0];
- };
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- this.clock.tick(1);
- assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist');
- assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist');
- // Simulate a live reload
- this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
- assert.equal(numCallsToSelectInitialPlaylistCalls, 1, 'selectInitialPlaylist');
- assert.equal(numCallsToSelectPlaylist, 0, 'selectPlaylist');
- });
- QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
- let resyncs = 0;
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- 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
- this.standardXHRResponse(this.requests.shift());
- // media
- this.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('fast quality change resyncs audio segment loader', function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'alternate-audio-multiple-groups.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- masterPlaylistController.selectPlaylist = () => {
- return masterPlaylistController.master().playlists[0];
- };
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- masterPlaylistController.mediaSource.trigger('sourceopen');
- this.player.audioTracks()[0].enabled = true;
- let resyncs = 0;
- let resets = 0;
- let realReset = masterPlaylistController.audioSegmentLoader_.resetLoader;
- masterPlaylistController.audioSegmentLoader_.resetLoader = function() {
- resets++;
- realReset.call(this);
- };
- masterPlaylistController.audioSegmentLoader_.resyncLoader = () => resyncs++;
- masterPlaylistController.fastQualityChange_();
- assert.equal(resyncs, 0, 'does not resync the audio segment loader when media same');
- // force different media
- masterPlaylistController.selectPlaylist = () => {
- return masterPlaylistController.master().playlists[1];
- };
- assert.equal(this.requests.length, 1, 'one request');
- masterPlaylistController.fastQualityChange_();
- assert.equal(this.requests.length, 2, 'added a request for new media');
- assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
- // new media request
- this.standardXHRResponse(this.requests[1]);
- assert.equal(resyncs, 1, 'resyncs the audio segment loader when media changes');
- assert.equal(resets, 0, 'does not reset the audio segment loader when media changes');
- });
- QUnit.test('audio segment loader is reset on audio track change', function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'alternate-audio-multiple-groups.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- masterPlaylistController.selectPlaylist = () => {
- return masterPlaylistController.master().playlists[0];
- };
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- masterPlaylistController.mediaSource.trigger('sourceopen');
- let resyncs = 0;
- let resets = 0;
- let realReset = masterPlaylistController.audioSegmentLoader_.resetLoader;
- masterPlaylistController.audioSegmentLoader_.resetLoader = function() {
- resets++;
- realReset.call(this);
- };
- masterPlaylistController.audioSegmentLoader_.resyncLoader = () => resyncs++;
- assert.equal(this.requests.length, 1, 'one request');
- assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
- this.player.audioTracks()[1].enabled = true;
- assert.equal(this.requests.length, 2, 'two requests');
- assert.equal(resyncs, 1, 'resyncs the audio segment loader when audio track changes');
- assert.equal(resets, 1, 'resets the audio segment loader when audio track changes');
- });
- 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.clock.tick(1);
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // Make segment metadata noop since most test segments dont have real data
- this.masterPlaylistController.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
- // 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
- this.standardXHRResponse(this.requests[0]);
- this.masterPlaylistController.mainSegmentLoader_.sourceUpdater_.buffered = () => {
- return videojs.createTimeRanges([[0, 20]]);
- };
- // 1ms has passed to upload 1kb
- // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
- this.clock.tick(1);
- // segment
- this.standardXHRResponse(this.requests[1]);
- this.masterPlaylistController.mainSegmentLoader_.fetchAtBuffer_ = true;
- this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
- this.clock.tick(10 * 1000);
- this.clock.tick(1);
- 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
- this.standardXHRResponse(this.requests.shift());
- // playlist
- this.standardXHRResponse(this.requests.shift());
- // segment
- this.standardXHRResponse(this.requests.shift());
- // change the source
- this.player.src({
- src: 'manifest/master.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // Make segment metadata noop since most test segments dont have real data
- this.masterPlaylistController.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
- // 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
- this.standardXHRResponse(this.requests.shift());
- // media
- this.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
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- this.player.tech_.on('progress', function() {
- progressCount++;
- });
- // 1ms has passed to upload 1kb
- // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
- this.clock.tick(1);
- // segment
- this.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 active loader when switching from unmuxed to muxed audio group',
- function(assert) {
- openMediaSource(this.player, this.clock);
- // master
- this.requests.shift().respond(200, null,
- manifests.multipleAudioGroupsCombinedMain);
- // media
- this.standardXHRResponse(this.requests.shift());
- // init segment
- this.standardXHRResponse(this.requests.shift());
- // video segment
- this.standardXHRResponse(this.requests.shift());
- // audio media
- this.standardXHRResponse(this.requests.shift());
- // ignore audio segment requests
- this.requests.length = 0;
- let mpc = this.masterPlaylistController;
- let combinedPlaylist = mpc.master().playlists[0];
- assert.ok(mpc.mediaTypes_.AUDIO.activePlaylistLoader,
- 'starts with an active playlist loader');
- 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.notOk(mpc.mediaTypes_.AUDIO.activePlaylistLoader,
- 'enabled a track in the new audio group');
- });
- QUnit.test('waits for both main and audio loaders to finish before calling endOfStream',
- function(assert) {
- openMediaSource(this.player, this.clock);
- const videoMedia = '#EXTM3U\n' +
- '#EXT-X-VERSION:3\n' +
- '#EXT-X-PLAYLIST-TYPE:VOD\n' +
- '#EXT-X-MEDIA-SEQUENCE:0\n' +
- '#EXT-X-TARGETDURATION:10\n' +
- '#EXTINF:10,\n' +
- 'video-0.ts\n' +
- '#EXT-X-ENDLIST\n';
- const audioMedia = '#EXTM3U\n' +
- '#EXT-X-VERSION:3\n' +
- '#EXT-X-PLAYLIST-TYPE:VOD\n' +
- '#EXT-X-MEDIA-SEQUENCE:0\n' +
- '#EXT-X-TARGETDURATION:10\n' +
- '#EXTINF:10,\n' +
- 'audio-0.ts\n' +
- '#EXT-X-ENDLIST\n';
- let videoEnded = 0;
- let audioEnded = 0;
- const MPC = this.masterPlaylistController;
- MPC.mainSegmentLoader_.on('ended', () => videoEnded++);
- MPC.audioSegmentLoader_.on('ended', () => audioEnded++);
- // master
- this.standardXHRResponse(this.requests.shift(), manifests.demuxed);
- // video media
- this.standardXHRResponse(this.requests.shift(), videoMedia);
- // audio media
- this.standardXHRResponse(this.requests.shift(), audioMedia);
- // video segment
- this.standardXHRResponse(this.requests.shift());
- MPC.mediaSource.sourceBuffers[0].trigger('updateend');
- assert.equal(videoEnded, 1, 'main segment loader triggered endded');
- assert.equal(audioEnded, 0, 'audio segment loader did not trigger ended');
- assert.equal(MPC.mediaSource.readyState, 'open', 'Media Source not yet ended');
- // audio segment
- this.standardXHRResponse(this.requests.shift());
- MPC.mediaSource.sourceBuffers[1].trigger('updateend');
- assert.equal(videoEnded, 1, 'main segment loader did not trigger ended again');
- assert.equal(audioEnded, 1, 'audio segment loader triggered ended');
- assert.equal(MPC.mediaSource.readyState, 'ended', 'Media Source ended');
- });
- QUnit.test('Segment loaders are unpaused when seeking after player has ended',
- function(assert) {
- openMediaSource(this.player, this.clock);
- const videoMedia = '#EXTM3U\n' +
- '#EXT-X-VERSION:3\n' +
- '#EXT-X-PLAYLIST-TYPE:VOD\n' +
- '#EXT-X-MEDIA-SEQUENCE:0\n' +
- '#EXT-X-TARGETDURATION:10\n' +
- '#EXTINF:10,\n' +
- 'video-0.ts\n' +
- '#EXT-X-ENDLIST\n';
- let ended = 0;
- this.masterPlaylistController.mainSegmentLoader_.on('ended', () => ended++);
- this.player.tech_.trigger('play');
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift(), videoMedia);
- // segment
- this.standardXHRResponse(this.requests.shift());
- assert.notOk(this.masterPlaylistController.mainSegmentLoader_.paused(),
- 'segment loader not yet paused');
- this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
- assert.ok(this.masterPlaylistController.mainSegmentLoader_.paused(),
- 'segment loader is paused after ending');
- assert.equal(ended, 1, 'segment loader triggered ended event');
- this.player.currentTime(5);
- this.clock.tick(1);
- assert.notOk(this.masterPlaylistController.mainSegmentLoader_.paused(),
- 'segment loader unpaused after a seek');
- assert.equal(ended, 1, 'segment loader did not trigger ended event again yet');
- });
- QUnit.test('detects if the player is stuck at the playlist end', function(assert) {
- let playlistCopy = Hls.Playlist.playlistEnd;
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- this.standardXHRResponse(this.requests.shift());
- let playlist = this.player.tech_.hls.selectPlaylist();
- // not stuck at playlist end when no seekable, even if empty buffer
- // and positive currentTime
- this.masterPlaylistController.seekable = () => videojs.createTimeRange();
- this.player.tech_.buffered = () => videojs.createTimeRange();
- this.player.tech_.setCurrentTime(170);
- assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
- 'not stuck at playlist end');
- // not stuck at playlist end when no seekable, even if empty buffer
- // and currentTime 0
- this.player.tech_.setCurrentTime(0);
- assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
- 'not stuck at playlist end');
- // not stuck at playlist end when no seekable but current time is at
- // the end of the buffered range
- this.player.tech_.buffered = () => videojs.createTimeRange(0, 170);
- assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
- 'not stuck at playlist end');
- // not stuck at playlist end when currentTime not at seekable end
- // even if the buffer is empty
- this.masterPlaylistController.seekable = () => videojs.createTimeRange(0, 130);
- this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
- this.player.tech_.setCurrentTime(50);
- this.player.tech_.buffered = () => videojs.createTimeRange();
- Hls.Playlist.playlistEnd = () => 130;
- assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
- 'not stuck at playlist end');
- // not stuck at playlist end when buffer reached the absolute end of the playlist
- // and current time is in the buffered range
- this.player.tech_.setCurrentTime(159);
- this.player.tech_.buffered = () => videojs.createTimeRange(0, 160);
- Hls.Playlist.playlistEnd = () => 160;
- assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
- 'not stuck at playlist end');
- // stuck at playlist end when there is no buffer and playhead
- // reached absolute end of playlist
- this.player.tech_.setCurrentTime(160);
- assert.ok(this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
- 'stuck at playlist end');
- // stuck at playlist end when current time reached the buffer end
- // and buffer has reached absolute end of playlist
- this.masterPlaylistController.seekable = () => videojs.createTimeRange(90, 130);
- this.player.tech_.buffered = () => videojs.createTimeRange(0, 170);
- this.player.tech_.setCurrentTime(170);
- Hls.Playlist.playlistEnd = () => 170;
- assert.ok(this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
- 'stuck at playlist end');
- Hls.Playlist.playlistEnd = playlistCopy;
- });
- 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
- this.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
- this.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
- this.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
- this.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,
- undefined,
- 'not excluded incompatible playlist');
- // verify stats
- assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
- });
- 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
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
- updates.push(update);
- };
- // 1ms has passed to upload 1kb
- // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
- this.clock.tick(1);
- this.masterPlaylistController.mainSegmentLoader_.mediaIndex = 0;
- // downloading the new segment will update bandwidth and cause a
- // playlist change
- // segment 0
- this.standardXHRResponse(this.requests.shift());
- // update the buffer to reflect the appended segment, and have enough buffer to
- // change playlist
- this.masterPlaylistController.tech_.buffered = () => {
- return videojs.createTimeRanges([[0, 30]]);
- };
- this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
- // media
- this.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
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- // "downloaded" a segment
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.strictEqual(calls, 2, 'selects after the initial segment');
- // and another
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.strictEqual(calls, 3, 'selects after additional segments');
- // verify stats
- assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
- });
- QUnit.test('re-triggers bandwidthupdate events on the tech', function(assert) {
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- let bandwidthupdateEvents = 0;
- this.player.tech_.on('bandwidthupdate', () => bandwidthupdateEvents++);
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(bandwidthupdateEvents, 1, 'triggered bandwidthupdate');
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(bandwidthupdateEvents, 2, 'triggered bandwidthupdate');
- });
- QUnit.test('switches to lower renditions immediately, higher dependent on buffer',
- function(assert) {
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- let buffered = [];
- let currentPlaylistBandwidth = 0;
- let nextPlaylistBandwidth = 0;
- let mediaChanges = [];
- let currentTime = 0;
- let endList = true;
- let duration = 100;
- this.masterPlaylistController.tech_.currentTime = () => currentTime;
- this.masterPlaylistController.tech_.buffered = () => videojs.createTimeRanges(buffered);
- this.masterPlaylistController.duration = () => duration;
- this.masterPlaylistController.selectPlaylist = () => {
- return {
- attributes: {
- BANDWIDTH: nextPlaylistBandwidth
- },
- endList
- };
- };
- this.masterPlaylistController.masterPlaylistLoader_.media = (media) => {
- if (!media) {
- return {
- attributes: {
- BANDWIDTH: currentPlaylistBandwidth
- },
- endList
- };
- }
- mediaChanges.push(media);
- };
- currentTime = 0;
- currentPlaylistBandwidth = 1000;
- nextPlaylistBandwidth = 1000;
- buffered = [];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 1,
- 'changes media when no buffer and equal bandwidth playlist');
- buffered = [[0, 9]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 2,
- 'changes media when sufficient forward buffer and equal ' +
- 'bandwidth playlist');
- buffered = [[0, 30]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 3,
- 'changes media when sufficient forward buffer and equal ' +
- 'bandwidth playlist');
- mediaChanges.length = 0;
- currentTime = 10;
- currentPlaylistBandwidth = 1000;
- nextPlaylistBandwidth = 1001;
- buffered = [];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 0,
- 'did not change media when no buffer and and higher bandwidth playlist');
- buffered = [[0, 19]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 0,
- 'did not change media when insufficient forward buffer and higher ' +
- 'bandwidth playlist');
- buffered = [[0, 20]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 1,
- 'changes media when sufficient forward buffer and higher ' +
- 'bandwidth playlist');
- buffered = [[0, 21]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 2,
- 'changes media when sufficient forward buffer and higher ' +
- 'bandwidth playlist');
- mediaChanges.length = 0;
- currentTime = 100;
- currentPlaylistBandwidth = 1000;
- nextPlaylistBandwidth = 1001;
- buffered = [];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 0,
- 'did not change media when no buffer and higher bandwidth playlist');
- buffered = [[0, 100], [100, 109]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 0,
- 'did not change media when insufficient forward buffer and higher ' +
- 'bandwidth playlist');
- buffered = [[0, 100], [100, 130]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 1,
- 'changes media when sufficient forward buffer and higher ' +
- 'bandwidth playlist');
- mediaChanges.length = 0;
- buffered = [];
- currentPlaylistBandwidth = 1000;
- nextPlaylistBandwidth = 999;
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 1,
- 'changes media when no buffer but lower bandwidth playlist');
- buffered = [[100, 109]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 2,
- 'changes media when insufficient forward buffer but lower ' +
- 'bandwidth playlist');
- buffered = [[100, 110]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 3,
- 'changes media when sufficient forward buffer and lower ' +
- 'bandwidth playlist');
- mediaChanges.length = 0;
- endList = false;
- currentTime = 100;
- currentPlaylistBandwidth = 1000;
- nextPlaylistBandwidth = 1001;
- buffered = [];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 1,
- 'changes live media when no buffer and higher bandwidth playlist');
- buffered = [[0, 100], [100, 109]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 2,
- 'changes live media when insufficient forward buffer and higher ' +
- 'bandwidth playlist');
- buffered = [[0, 100], [100, 130]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 3,
- 'changes live media when sufficient forward buffer and higher ' +
- 'bandwidth playlist');
- mediaChanges.length = 0;
- endList = true;
- currentTime = 9;
- duration = 18;
- buffered = [];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 1,
- 'changes media when no buffer and duration less than low water line');
- buffered = [[0, 10]];
- this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
- assert.equal(mediaChanges.length,
- 2,
- 'changes media when insufficient forward buffer and duration ' +
- 'less than low water line');
- });
- QUnit.test('blacklists playlist on earlyabort', function(assert) {
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- let mediaChanges = [];
- const playlistLoader = this.masterPlaylistController.masterPlaylistLoader_;
- const currentMedia = playlistLoader.media();
- const origMedia = playlistLoader.media.bind(playlistLoader);
- const origWarn = videojs.log.warn;
- let warnings = [];
- this.masterPlaylistController.masterPlaylistLoader_.media = (media) => {
- if (media) {
- mediaChanges.push(media);
- }
- return origMedia(media);
- };
- videojs.log.warn = (text) => warnings.push(text);
- assert.notOk(currentMedia.excludeUntil > 0, 'playlist not blacklisted');
- assert.equal(mediaChanges.length, 0, 'no media change');
- this.masterPlaylistController.mainSegmentLoader_.trigger('earlyabort');
- assert.ok(currentMedia.excludeUntil > 0, 'playlist blacklisted');
- assert.equal(mediaChanges.length, 1, 'one media change');
- assert.equal(warnings.length, 1, 'one warning logged');
- assert.equal(warnings[0],
- 'Problem encountered with the current HLS playlist. ' +
- 'Aborted early because there isn\'t enough bandwidth to complete the ' +
- 'request without rebuffering. Switching to another playlist.',
- 'warning message is correct');
- videojs.log.warn = origWarn;
- });
- QUnit.test('does not get stuck in a loop due to inconsistent network/caching',
- function(assert) {
- /*
- * This test is a long one, but it is meant to follow a true path to a possible loop.
- * The reason for the loop is due to inconsistent network bandwidth, often caused or
- * amplified by caching at the browser or edge server level.
- * The steps are as follows:
- *
- * 1) Request segment 0 from low bandwidth playlist
- * 2) Request segment 1 from low bandwidth playlist
- * 3) Switch up due to good bandwidth (2 segments are required before upswitching)
- * 4) Request segment 0 from high bandwidth playlist
- * 5) Abort request early due to low bandwidth
- * 6) Request segment 0 from low bandwidth playlist
- * 7) Request segment 1 from low bandwidth playlist
- * 8) Request segment 2 from low bandwidth playlist, despite enough bandwidth to
- * upswitch. This part is the key, as the behavior we want to avoid is an upswitch
- * back to the high bandwidth playlist (thus starting a potentially infinite loop).
- */
- const mediaContents =
- '#EXTM3U\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' +
- '#EXT-X-ENDLIST\n';
- const segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
- // start on lowest bandwidth rendition (will be media.m3u8)
- segmentLoader.bandwidth = 0;
- this.player.tech_.paused = () => false;
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- // master
- this.requests.shift().respond(200, null,
- '#EXTM3U\n' +
- '#EXT-X-STREAM-INF:BANDWIDTH=10\n' +
- 'media.m3u8\n' +
- '#EXT-X-STREAM-INF:BANDWIDTH=100\n' +
- 'media1.m3u8\n');
- // media.m3u8
- this.requests.shift().respond(200, null, mediaContents);
- let playlistLoader = this.masterPlaylistController.masterPlaylistLoader_;
- let origMedia = playlistLoader.media.bind(playlistLoader);
- let mediaChanges = [];
- this.masterPlaylistController.masterPlaylistLoader_.media = (media) => {
- if (media) {
- mediaChanges.push(media);
- }
- return origMedia(media);
- };
- this.clock.tick(1);
- let segmentRequest = this.requests[0];
- assert.equal(segmentRequest.uri.substring(segmentRequest.uri.length - 4),
- '0.ts',
- 'requested first segment');
- // 100ms for the segment response
- this.clock.tick(100);
- // 10 bytes in 100ms = 800 bits/s
- this.requests[0].response = new Uint8Array(10).buffer;
- this.requests.shift().respond(200, null, '');
- segmentLoader.mediaSource_.sourceBuffers[0].trigger('updateend');
- this.clock.tick(1);
- segmentRequest = this.requests[0];
- // should be walking forwards (need two segments before we can switch)
- assert.equal(segmentLoader.bandwidth, 800, 'bandwidth is correct');
- assert.equal(segmentRequest.uri.substring(segmentRequest.uri.length - 4),
- '1.ts',
- 'requested second segment');
- assert.equal(mediaChanges.length, 0, 'no media changes');
- // 100ms for the segment response
- this.clock.tick(100);
- // 11 bytes in 100ms = 880 bits/s
- this.requests[0].response = new Uint8Array(11).buffer;
- this.requests.shift().respond(200, null, '');
- segmentLoader.mediaSource_.sourceBuffers[0].trigger('updateend');
- this.clock.tick(1);
- let mediaRequest = this.requests[0];
- // after two segments, bandwidth is high enough to switch up to media1.m3u8
- assert.equal(segmentLoader.bandwidth, 880, 'bandwidth is correct');
- assert.equal(mediaChanges.length, 1, 'changed media');
- assert.equal(mediaChanges[0].uri, 'media1.m3u8', 'changed to media1');
- assert.equal(mediaRequest.uri.substring(mediaRequest.uri.length - 'media1.m3u8'.length),
- 'media1.m3u8',
- 'requested media1');
- // media1.m3u8
- this.requests.shift().respond(200, null, mediaContents);
- this.clock.tick(1);
- segmentRequest = this.requests[0];
- assert.equal(segmentLoader.playlist_.uri,
- 'media1.m3u8',
- 'segment loader playlist is media1');
- const media1ResolvedPlaylist = segmentLoader.playlist_;
- assert.notOk(media1ResolvedPlaylist.excludeUntil, 'media1 not blacklisted');
- assert.equal(segmentRequest.uri.substring(segmentRequest.uri.length - 4),
- '0.ts',
- 'requested first segment');
- // needs a timeout for early abort to occur (we skip the function otherwise, since no
- // timeout means we are on the last rendition)
- segmentLoader.xhrOptions_.timeout = 60000;
- // we need to wait 1 second from first byte receieved in order to consider aborting
- this.requests[0].downloadProgress({
- target: this.requests[0],
- total: 100,
- loaded: 1
- });
- this.clock.tick(1000);
- // should abort request early because we don't have enough bandwidth
- this.requests[0].downloadProgress({
- target: this.requests[0],
- total: 100,
- // 1 bit per second
- loaded: 2
- });
- this.clock.tick(1);
- // aborted request, so switched back to lowest rendition
- assert.equal(segmentLoader.bandwidth,
- 10 * Config.BANDWIDTH_VARIANCE + 1,
- 'bandwidth is correct for abort');
- assert.equal(mediaChanges.length, 2, 'changed media');
- assert.equal(mediaChanges[1].uri, 'media.m3u8', 'changed to media');
- assert.ok(media1ResolvedPlaylist.excludeUntil, 'blacklisted media1');
- assert.equal(segmentRequest.uri.substring(segmentRequest.uri.length - 4),
- '0.ts',
- 'requested first segment');
- // remove aborted request
- this.requests.shift();
- // 1ms for the cached segment response
- this.clock.tick(1);
- // 10 bytes in 1ms = 80 kbps
- this.requests[0].response = new Uint8Array(10).buffer;
- this.requests.shift().respond(200, null, '');
- segmentLoader.mediaSource_.sourceBuffers[0].trigger('updateend');
- this.clock.tick(1);
- segmentRequest = this.requests[0];
- // walking forwards, still need two segments before trying to change rendition
- assert.equal(segmentLoader.bandwidth, 80000, 'bandwidth is correct');
- assert.equal(mediaChanges.length, 2, 'did not change media');
- assert.equal(segmentRequest.uri.substring(segmentRequest.uri.length - 4),
- '1.ts',
- 'requested second segment');
- // 1ms for the cached segment response
- this.clock.tick(1);
- // 11 bytes in 1ms = 88 kbps
- this.requests[0].response = new Uint8Array(11).buffer;
- this.requests.shift().respond(200, null, '');
- segmentLoader.mediaSource_.sourceBuffers[0].trigger('updateend');
- this.clock.tick(1);
- // Media may be changed, but it should be changed to the same media. In the future, this
- // can safely not be changed.
- assert.equal(segmentLoader.bandwidth, 88000, 'bandwidth is correct');
- assert.equal(mediaChanges.length, 3, 'changed media');
- assert.equal(mediaChanges[2].uri, 'media.m3u8', 'media remains unchanged');
- segmentRequest = this.requests[0];
- assert.equal(segmentRequest.uri.substring(segmentRequest.uri.length - 4),
- '2.ts',
- 'requested third segment');
- assert.equal(this.env.log.warn.callCount, 1, 'logged a warning');
- this.env.log.warn.callCount = 0;
- });
- QUnit.test('updates the duration after switching playlists', function(assert) {
- let selectedPlaylist = false;
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- this.masterPlaylistController.bandwidth = 1e20;
- // master
- this.standardXHRResponse(this.requests[0]);
- // media
- this.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 has passed to upload 1kb
- // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
- this.clock.tick(1);
- this.masterPlaylistController.mainSegmentLoader_.mediaIndex = 0;
- // segment 0
- this.standardXHRResponse(this.requests[2]);
- this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
- // media1
- this.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
- this.standardXHRResponse(this.requests[0]);
- // media
- this.standardXHRResponse(this.requests[1]);
- assert.ok(/media3\.m3u8/i.test(this.requests[1].url), 'Selected the highest rendition');
- // 1ms has passed to upload 1kb
- // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
- this.clock.tick(1);
- this.masterPlaylistController.mainSegmentLoader_.mediaIndex = 0;
- // segment 0
- this.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
- this.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
- this.standardXHRResponse(this.requests[0]);
- // media
- this.standardXHRResponse(this.requests[1]);
- assert.equal(this.masterPlaylistController.requestOptions_.timeout,
- this.masterPlaylistController.masterPlaylistLoader_.targetDuration * 1.5 *
- 1000,
- 'default request timeout');
- assert.ok(!Playlist.isLowestEnabledRendition(
- this.masterPlaylistController.masterPlaylistLoader_.master,
- this.masterPlaylistController.masterPlaylistLoader_.media()),
- 'not on 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
- this.standardXHRResponse(this.requests[2]);
- // Download new segment after media change
- this.standardXHRResponse(this.requests[3]);
- assert.ok(Playlist.isLowestEnabledRendition(
- this.masterPlaylistController.masterPlaylistLoader_.master,
- this.masterPlaylistController.masterPlaylistLoader_.media()),
- 'on lowest rendition');
- assert.equal(this.masterPlaylistController.requestOptions_.timeout, 0,
- 'request timeout 0');
- });
- QUnit.test('removes request timeout when the source is a media playlist and not master',
- function(assert) {
- this.requests.length = 0;
- this.player.src({
- src: 'manifest/media.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // media
- this.standardXHRResponse(this.requests.shift());
- assert.equal(this.masterPlaylistController.requestOptions_.timeout, 0,
- 'request timeout set to 0 when loading a non master playlist');
- });
- 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;
- this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
- 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.mediaTypes_.AUDIO.activePlaylistLoader = {
- 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');
- mainTimeRanges = [[1, 10]];
- audioTimeRanges = [[11, 20]];
- mpc.seekable_ = videojs.createTimeRanges();
- mpc.onSyncInfoUpdate_();
- assertTimeRangesEqual(mpc.seekable(),
- videojs.createTimeRanges([[1, 10]]),
- 'no intersection, audio later');
- mainTimeRanges = [[11, 20]];
- audioTimeRanges = [[1, 10]];
- mpc.seekable_ = videojs.createTimeRanges();
- mpc.onSyncInfoUpdate_();
- assertTimeRangesEqual(mpc.seekable(),
- videojs.createTimeRanges([[11, 20]]),
- 'no intersection, main later');
- Playlist.seekable = origSeekable;
- });
- QUnit.test('syncInfoUpdate triggers seekablechanged when seekable is updated',
- function(assert) {
- let origSeekable = Playlist.seekable;
- let mpc = this.masterPlaylistController;
- let tech = this.player.tech_;
- let mainTimeRanges = [];
- let media = {};
- let seekablechanged = 0;
- tech.on('seekablechanged', () => seekablechanged++);
- Playlist.seekable = () => {
- return videojs.createTimeRanges(mainTimeRanges);
- };
- this.masterPlaylistController.masterPlaylistLoader_.media = () => media;
- this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
- mainTimeRanges = [[0, 10]];
- mpc.seekable_ = videojs.createTimeRanges();
- mpc.onSyncInfoUpdate_();
- assert.equal(seekablechanged, 1, 'seekablechanged triggered');
- 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.clock.tick(1);
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- let callCount = 0;
- this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
- // master
- this.standardXHRResponse(this.requests.shift());
- assert.equal(callCount, 0, 'no call to update cues on master');
- // media
- this.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.clock.tick(1);
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- this.masterPlaylistController.useCueTags_ = true;
- let callCount = 0;
- this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
- // media
- this.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;
- let hlsPlaylistCueTagsEvents = 0;
- videojs.options.hls = {
- useCueTags: true
- };
- this.player = createPlayer();
- this.player.tech_.on('usage', (event) => {
- if (event.name === 'hls-playlist-cue-tags') {
- hlsPlaylistCueTagsEvents++;
- }
- });
- this.player.src({
- src: 'manifest/media.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- this.standardXHRResponse(this.requests.shift());
- this.standardXHRResponse(this.requests.shift());
- assert.equal(hlsPlaylistCueTagsEvents, 1, 'cue tags event has been triggered once');
- 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.test('correctly sets alternate audio track kinds', function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/alternate-audio-accessibility.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- // master
- this.standardXHRResponse(this.requests.shift());
- // media - required for loadedmetadata
- this.standardXHRResponse(this.requests.shift());
- const audioTracks = this.player.tech_.audioTracks();
- assert.equal(audioTracks.length, 4, 'added 4 audio tracks');
- assert.equal(audioTracks[0].id, 'English', 'contains english track');
- assert.equal(audioTracks[0].kind, 'main', 'english track\'s kind is "main"');
- assert.equal(audioTracks[1].id,
- 'English Descriptions',
- 'contains english descriptions track');
- assert.equal(audioTracks[1].kind,
- 'main-desc',
- 'english descriptions track\'s kind is "main-desc"');
- assert.equal(audioTracks[2].id, 'Français', 'contains french track');
- assert.equal(audioTracks[2].kind,
- 'alternative',
- 'french track\'s kind is "alternative"');
- assert.equal(audioTracks[3].id, 'Espanol', 'contains spanish track');
- assert.equal(audioTracks[3].kind,
- 'alternative',
- 'spanish track\'s kind is "alternative"');
- });
- QUnit.test('trigger events when video and audio is demuxed by default', function(assert) {
- let hlsDemuxedEvents = 0;
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/multipleAudioGroups.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.player.tech_.on('usage', (event) => {
- if (event.name === 'hls-demuxed') {
- hlsDemuxedEvents++;
- }
- });
- openMediaSource(this.player, this.clock);
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- assert.equal(hlsDemuxedEvents, 1, 'video and audio is demuxed by default');
- });
- QUnit.test('trigger events when an AES is detected', function(assert) {
- let hlsAesEvents = 0;
- let isAesCopy = Hls.Playlist.isAes;
- Hls.Playlist.isAes = (media) => {
- return true;
- };
- this.player.tech_.on('usage', (event) => {
- if (event.name === 'hls-aes') {
- hlsAesEvents++;
- }
- });
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- assert.equal(hlsAesEvents, 1, 'an AES HLS stream is detected');
- Hls.Playlist.isAes = isAesCopy;
- });
- QUnit.test('trigger events when an fMP4 stream is detected', function(assert) {
- let hlsFmp4Events = 0;
- let isFmp4Copy = Hls.Playlist.isFmp4;
- Hls.Playlist.isFmp4 = (media) => {
- return true;
- };
- this.player.tech_.on('usage', (event) => {
- if (event.name === 'hls-fmp4') {
- hlsFmp4Events++;
- }
- });
- // master
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- this.masterPlaylistController.mediaSource.trigger('sourceopen');
- assert.equal(hlsFmp4Events, 1, 'an fMP4 stream is detected');
- Hls.Playlist.isFmp4 = isFmp4Copy;
- });
- QUnit.test('adds only CEA608 closed-caption tracks when a master playlist is loaded',
- function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master-captions.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- // wait for async player.src to complete
- this.clock.tick(1);
- const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- assert.equal(this.player.textTracks().length, 1, 'one text track to start');
- assert.equal(this.player.textTracks()[0].label,
- 'segment-metadata',
- 'only segment-metadata text track');
- // master, contains media groups for captions
- this.standardXHRResponse(this.requests.shift());
- // we wait for loadedmetadata before setting caption tracks, so we need to wait for a
- // media playlist
- assert.equal(this.player.textTracks().length, 1, 'only one text track after master');
- // media
- this.standardXHRResponse(this.requests.shift());
- const master = masterPlaylistController.masterPlaylistLoader_.master;
- const caps = master.mediaGroups['CLOSED-CAPTIONS'].CCs;
- const capsArr = Object.keys(caps).map(key => Object.assign({name: key}, caps[key]));
- const addedCaps = masterPlaylistController.mediaTypes_['CLOSED-CAPTIONS'].groups.CCs
- .map(cap => Object.assign({name: cap.id}, cap));
- assert.equal(capsArr.length, 4, '4 closed-caption tracks defined in playlist');
- assert.equal(addedCaps.length, 2, '2 CEA608 tracks added internally');
- assert.equal(addedCaps[0].instreamId, 'CC1', 'first 608 track is CC1');
- assert.equal(addedCaps[1].instreamId, 'CC3', 'second 608 track is CC3');
- const textTracks = this.player.textTracks();
- assert.equal(textTracks.length, 3, '2 text tracks were added');
- assert.equal(textTracks[1].mode, 'disabled', 'track starts disabled');
- assert.equal(textTracks[2].mode, 'disabled', 'track starts disabled');
- assert.equal(textTracks[1].id, addedCaps[0].instreamId,
- 'text track 1\'s id is CC\'s instreamId');
- assert.equal(textTracks[2].id, addedCaps[1].instreamId,
- 'text track 2\'s id is CC\'s instreamId');
- assert.equal(textTracks[1].label, addedCaps[0].name,
- 'text track 1\'s label is CC\'s name');
- assert.equal(textTracks[2].label, addedCaps[1].name,
- 'text track 2\'s label is CC\'s name');
- });
- QUnit.test('adds subtitle tracks when a media playlist is loaded', function(assert) {
- let hlsWebvttEvents = 0;
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master-subtitles.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- this.player.tech_.on('usage', (event) => {
- if (event.name === 'hls-webvtt') {
- hlsWebvttEvents++;
- }
- });
- const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- assert.equal(hlsWebvttEvents, 0, 'there is no webvtt detected');
- assert.equal(this.player.textTracks().length, 1, 'one text track to start');
- assert.equal(this.player.textTracks()[0].label,
- 'segment-metadata',
- 'only segment-metadata text track');
- // master, contains media groups for subtitles
- this.standardXHRResponse(this.requests.shift());
- // we wait for loadedmetadata before setting subtitle tracks, so we need to wait for a
- // media playlist
- assert.equal(this.player.textTracks().length, 1, 'only one text track after master');
- // media
- this.standardXHRResponse(this.requests.shift());
- const master = masterPlaylistController.masterPlaylistLoader_.master;
- const subs = master.mediaGroups.SUBTITLES.subs;
- const subsArr = Object.keys(subs).map(key => subs[key]);
- assert.equal(subsArr.length, 4, 'got 4 subtitles');
- assert.equal(subsArr.filter(sub => sub.forced === false).length, 2, '2 forced');
- assert.equal(subsArr.filter(sub => sub.forced === true).length, 2, '2 non-forced');
- const textTracks = this.player.textTracks();
- assert.equal(textTracks.length, 3, 'non-forced text tracks were added');
- assert.equal(textTracks[1].mode, 'disabled', 'track starts disabled');
- assert.equal(textTracks[2].mode, 'disabled', 'track starts disabled');
- assert.equal(hlsWebvttEvents, 1, 'there is webvtt detected in the rendition');
- // change source to make sure tracks are cleaned up
- this.player.src({
- src: 'http://example.com/media.mp4',
- type: 'video/mp4'
- });
- this.clock.tick(1);
- assert.equal(this.player.textTracks().length, 0, 'text tracks cleaned');
- });
- QUnit.test('switches off subtitles on subtitle errors', function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master-subtitles.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // sets up listener for text track changes
- masterPlaylistController.trigger('sourceopen');
- // master, contains media groups for subtitles
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- const textTracks = this.player.textTracks();
- assert.equal(this.requests.length, 0, 'no outstanding requests');
- // enable first subtitle text track
- assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
- assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
- textTracks[1].mode = 'showing';
- assert.equal(this.requests.length, 1, 'made a request');
- assert.equal(textTracks[1].mode, 'showing', 'text track still showing');
- // request failed
- this.requests.shift().respond(404, null, '');
- assert.equal(textTracks[1].mode, 'disabled', 'disabled text track');
- assert.equal(this.env.log.warn.callCount, 1, 'logged a warning');
- this.env.log.warn.callCount = 0;
- assert.equal(this.requests.length, 0, 'no outstanding requests');
- // re-enable first text track
- textTracks[1].mode = 'showing';
- assert.equal(this.requests.length, 1, 'made a request');
- assert.equal(textTracks[1].mode, 'showing', 'text track still showing');
- this.requests.shift().respond(200, null, `
- #EXTM3U
- #EXT-X-TARGETDURATION:10
- #EXT-X-MEDIA-SEQUENCE:0
- #EXTINF:10
- 0.webvtt
- #EXT-X-ENDLIST
- `);
- const syncController = masterPlaylistController.subtitleSegmentLoader_.syncController_;
- // required for the vtt request to be made
- syncController.timestampOffsetForTimeline = () => 0;
- this.clock.tick(1);
- assert.equal(this.requests.length, 1, 'made a request');
- assert.ok(this.requests[0].url.endsWith('0.webvtt'), 'made a webvtt request');
- assert.equal(textTracks[1].mode, 'showing', 'text track still showing');
- this.requests.shift().respond(404, null, '');
- assert.equal(textTracks[1].mode, 'disabled', 'disabled text track');
- assert.equal(this.env.log.warn.callCount, 1, 'logged a warning');
- this.env.log.warn.callCount = 0;
- });
- QUnit.test('pauses subtitle segment loader on tech errors', function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master-subtitles.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // sets up listener for text track changes
- masterPlaylistController.trigger('sourceopen');
- // master, contains media groups for subtitles
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- const textTracks = this.player.textTracks();
- // enable first subtitle text track
- assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
- assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
- textTracks[1].mode = 'showing';
- let pauseCount = 0;
- masterPlaylistController.subtitleSegmentLoader_.pause = () => pauseCount++;
- this.player.tech_.trigger('error');
- assert.equal(pauseCount, 1, 'paused subtitle segment loader');
- });
- QUnit.test('disposes subtitle loaders on dispose', function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master-subtitles.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- let masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- assert.notOk(masterPlaylistController.mediaTypes_.SUBTITLES.activePlaylistLoader,
- 'does not start with a subtitle playlist loader');
- assert.ok(masterPlaylistController.subtitleSegmentLoader_,
- 'starts with a subtitle segment loader');
- let segmentLoaderDisposeCount = 0;
- masterPlaylistController.subtitleSegmentLoader_.dispose =
- () => segmentLoaderDisposeCount++;
- masterPlaylistController.dispose();
- assert.equal(segmentLoaderDisposeCount, 1, 'disposed the subtitle segment loader');
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master-subtitles.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // sets up listener for text track changes
- masterPlaylistController.trigger('sourceopen');
- // master, contains media groups for subtitles
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- const textTracks = this.player.textTracks();
- // enable first subtitle text track
- assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
- assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
- textTracks[1].mode = 'showing';
- assert.ok(masterPlaylistController.mediaTypes_.SUBTITLES.activePlaylistLoader,
- 'has a subtitle playlist loader');
- assert.ok(masterPlaylistController.subtitleSegmentLoader_,
- 'has a subtitle segment loader');
- let playlistLoaderDisposeCount = 0;
- segmentLoaderDisposeCount = 0;
- masterPlaylistController.mediaTypes_.SUBTITLES.activePlaylistLoader.dispose =
- () => playlistLoaderDisposeCount++;
- masterPlaylistController.subtitleSegmentLoader_.dispose =
- () => segmentLoaderDisposeCount++;
- masterPlaylistController.dispose();
- assert.equal(playlistLoaderDisposeCount, 1, 'disposed the subtitle playlist loader');
- assert.equal(segmentLoaderDisposeCount, 1, 'disposed the subtitle segment loader');
- });
- QUnit.test('subtitle segment loader resets on seeks', function(assert) {
- this.requests.length = 0;
- this.player = createPlayer();
- this.player.src({
- src: 'manifest/master-subtitles.m3u8',
- type: 'application/vnd.apple.mpegurl'
- });
- this.clock.tick(1);
- const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
- // sets up listener for text track changes
- masterPlaylistController.trigger('sourceopen');
- // master, contains media groups for subtitles
- this.standardXHRResponse(this.requests.shift());
- // media
- this.standardXHRResponse(this.requests.shift());
- const textTracks = this.player.textTracks();
- // enable first subtitle text track
- assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
- assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
- textTracks[1].mode = 'showing';
- let resetCount = 0;
- let abortCount = 0;
- let loadCount = 0;
- masterPlaylistController.subtitleSegmentLoader_.resetEverything = () => resetCount++;
- masterPlaylistController.subtitleSegmentLoader_.abort = () => abortCount++;
- masterPlaylistController.subtitleSegmentLoader_.load = () => loadCount++;
- this.player.pause();
- masterPlaylistController.setCurrentTime(5);
- assert.equal(resetCount, 1, 'reset subtitle segment loader');
- assert.equal(abortCount, 1, 'aborted subtitle segment loader');
- assert.equal(loadCount, 1, 'called load on subtitle segment loader');
- this.player.play();
- resetCount = 0;
- abortCount = 0;
- loadCount = 0;
- masterPlaylistController.setCurrentTime(10);
- assert.equal(resetCount, 1, 'reset subtitle segment loader');
- assert.equal(abortCount, 1, 'aborted subtitle segment loader');
- assert.equal(loadCount, 1, 'called load on subtitle segment loader');
- });
- QUnit.test('calculates dynamic GOAL_BUFFER_LENGTH', function(assert) {
- const configOld = {
- GOAL_BUFFER_LENGTH: Config.GOAL_BUFFER_LENGTH,
- MAX_GOAL_BUFFER_LENGTH: Config.MAX_GOAL_BUFFER_LENGTH,
- GOAL_BUFFER_LENGTH_RATE: Config.GOAL_BUFFER_LENGTH_RATE
- };
- const mpc = this.masterPlaylistController;
- let currentTime = 0;
- Config.GOAL_BUFFER_LENGTH = 30;
- Config.MAX_GOAL_BUFFER_LENGTH = 60;
- Config.GOAL_BUFFER_LENGTH_RATE = 0.5;
- mpc.tech_.currentTime = () => currentTime;
- assert.equal(mpc.goalBufferLength(), 30, 'dynamic GBL uses starting value at time 0');
- currentTime = 10;
- assert.equal(mpc.goalBufferLength(), 35, 'dynamic GBL increases by currentTime * rate');
- currentTime = 60;
- assert.equal(mpc.goalBufferLength(), 60, 'dynamic GBL uses max value');
- currentTime = 70;
- assert.equal(mpc.goalBufferLength(), 60, 'dynamic GBL continues to use max value');
- // restore config
- Object.keys(configOld).forEach((key) => Config[key] = configOld[key]);
- });
- QUnit.test('calculates dynamic BUFFER_LOW_WATER_LINE', function(assert) {
- const configOld = {
- BUFFER_LOW_WATER_LINE: Config.BUFFER_LOW_WATER_LINE,
- MAX_BUFFER_LOW_WATER_LINE: Config.MAX_BUFFER_LOW_WATER_LINE,
- BUFFER_LOW_WATER_LINE_RATE: Config.BUFFER_LOW_WATER_LINE_RATE
- };
- const mpc = this.masterPlaylistController;
- let currentTime = 0;
- Config.BUFFER_LOW_WATER_LINE = 0;
- Config.MAX_BUFFER_LOW_WATER_LINE = 30;
- Config.BUFFER_LOW_WATER_LINE_RATE = 0.5;
- mpc.tech_.currentTime = () => currentTime;
- assert.equal(mpc.bufferLowWaterLine(), 0, 'dynamic BLWL uses starting value at time 0');
- currentTime = 10;
- assert.equal(mpc.bufferLowWaterLine(), 5,
- 'dynamic BLWL increases by currentTime * rate');
- currentTime = 60;
- assert.equal(mpc.bufferLowWaterLine(), 30, 'dynamic BLWL uses max value');
- currentTime = 70;
- assert.equal(mpc.bufferLowWaterLine(), 30, 'dynamic BLWL continues to use max value');
- // restore config
- Object.keys(configOld).forEach((key) => Config[key] = configOld[key]);
- });
- QUnit.test('Exception in play promise should be caught', function(assert) {
- const mpc = this.masterPlaylistController;
- mpc.setupSourceBuffers = () => true;
- mpc.tech_ = {
- autoplay: () => true,
- play: () => new Promise(function(resolve, reject) {
- reject(new DOMException());
- })
- };
- mpc.handleSourceOpen_();
- assert.ok(true, 'rejects dom exception');
- });
- QUnit.module('Codec to MIME Type Conversion');
- const testMimeTypes = function(assert, isFMP4) {
- let container = isFMP4 ? 'mp4' : 'mp2t';
- let videoMime = `video/${container}`;
- let audioMime = `audio/${container}`;
- // no MAAT
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(false, true, false, false, isFMP4)),
- [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`],
- `no MAAT, container: ${container}, codecs: none`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(false, true, true, false, isFMP4)),
- [`${videoMime}; codecs="avc1.deadbeef"`],
- `no MAAT, container: ${container}, codecs: video`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(false, true, false, true, isFMP4)),
- [`${audioMime}; codecs="mp4a.40.E"`],
- `no MAAT, container: ${container}, codecs: audio`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(false, true, true, true, isFMP4)),
- [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`],
- `no MAAT, container: ${container}, codecs: video, audio`);
- // MAAT, not muxed
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, false, false, false, isFMP4)),
- [`${videoMime}; codecs="avc1.4d400d"`,
- `${audioMime}; codecs="mp4a.40.2"`],
- `MAAT, demuxed, container: ${container}, codecs: none`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, false, true, false, isFMP4)),
- [`${videoMime}; codecs="avc1.deadbeef"`,
- `${audioMime}; codecs="mp4a.40.2"`],
- `MAAT, demuxed, container: ${container}, codecs: video`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, false, false, true, isFMP4)),
- [`${videoMime}; codecs="mp4a.40.E"`,
- `${audioMime}; codecs="mp4a.40.E"`],
- `MAAT, demuxed, container: ${container}, codecs: audio`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, false, true, true, isFMP4)),
- [`${videoMime}; codecs="avc1.deadbeef"`,
- `${audioMime}; codecs="mp4a.40.E"`],
- `MAAT, demuxed, container: ${container}, codecs: video, audio`);
- // MAAT, muxed
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, true, false, false, isFMP4)),
- [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`,
- `${audioMime}; codecs="mp4a.40.2"`],
- `MAAT, muxed, container: ${container}, codecs: none`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, true, true, false, isFMP4)),
- [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.2"`,
- `${audioMime}; codecs="mp4a.40.2"`],
- `MAAT, muxed, container: ${container}, codecs: video`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, true, false, true, isFMP4)),
- [`${videoMime}; codecs="mp4a.40.E"`,
- `${audioMime}; codecs="mp4a.40.E"`],
- `MAAT, muxed, container: ${container}, codecs: audio`);
- assert.deepEqual(mimeTypesForPlaylist_.apply(null,
- generateMedia(true, true, true, true, isFMP4)),
- [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`,
- `${audioMime}; codecs="mp4a.40.E"`],
- `MAAT, muxed, container: ${container}, codecs: video, audio`);
- };
- QUnit.test('recognizes muxed codec configurations', function(assert) {
- testMimeTypes(assert, false);
- testMimeTypes(assert, true);
- });
- QUnit.module('Map Legacy AVC Codec');
- QUnit.test('maps legacy AVC codecs', function(assert) {
- assert.equal(mapLegacyAvcCodecs_('avc1.deadbeef'),
- 'avc1.deadbeef',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('avc1.dead.beef, mp4a.something'),
- 'avc1.dead.beef, mp4a.something',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('avc1.dead.beef,mp4a.something'),
- 'avc1.dead.beef,mp4a.something',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('mp4a.something,avc1.dead.beef'),
- 'mp4a.something,avc1.dead.beef',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('mp4a.something, avc1.dead.beef'),
- 'mp4a.something, avc1.dead.beef',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('avc1.42001e'),
- 'avc1.42001e',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('avc1.4d0020,mp4a.40.2'),
- 'avc1.4d0020,mp4a.40.2',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('mp4a.40.2,avc1.4d0020'),
- 'mp4a.40.2,avc1.4d0020',
- 'does nothing for non legacy pattern');
- assert.equal(mapLegacyAvcCodecs_('mp4a.40.40'),
- 'mp4a.40.40',
- 'does nothing for non video codecs');
- assert.equal(mapLegacyAvcCodecs_('avc1.66.30'),
- 'avc1.42001e',
- 'translates legacy video codec alone');
- assert.equal(mapLegacyAvcCodecs_('avc1.66.30, mp4a.40.2'),
- 'avc1.42001e, mp4a.40.2',
- 'translates legacy video codec when paired with audio');
- assert.equal(mapLegacyAvcCodecs_('mp4a.40.2, avc1.66.30'),
- 'mp4a.40.2, avc1.42001e',
- 'translates video codec when specified second');
- });
|