loader-common.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169
  1. import QUnit from 'qunit';
  2. import videojs from 'video.js';
  3. import xhrFactory from '../src/xhr';
  4. import Config from '../src/config';
  5. import {
  6. playlistWithDuration,
  7. useFakeEnvironment,
  8. useFakeMediaSource
  9. } from './test-helpers.js';
  10. import { MasterPlaylistController } from '../src/master-playlist-controller';
  11. import SyncController from '../src/sync-controller';
  12. import Decrypter from '../src/decrypter-worker';
  13. import worker from 'webwackify';
  14. const resolveDecrypterWorker = () => {
  15. let result;
  16. try {
  17. result = require.resolve('../src/decrypter-worker');
  18. } catch (e) {
  19. // no result
  20. }
  21. return result;
  22. };
  23. /**
  24. * beforeEach and afterEach hooks that should be run segment loader tests regardless of
  25. * the type of loader.
  26. */
  27. export const LoaderCommonHooks = {
  28. beforeEach(assert) {
  29. this.env = useFakeEnvironment(assert);
  30. this.clock = this.env.clock;
  31. this.requests = this.env.requests;
  32. this.mse = useFakeMediaSource();
  33. this.currentTime = 0;
  34. this.seekable = {
  35. length: 0
  36. };
  37. this.seeking = false;
  38. this.hasPlayed = true;
  39. this.paused = false;
  40. this.playbackRate = 1;
  41. this.fakeHls = {
  42. xhr: xhrFactory(),
  43. tech_: {
  44. paused: () => this.paused,
  45. playbackRate: () => this.playbackRate,
  46. currentTime: () => this.currentTime
  47. }
  48. };
  49. this.tech_ = this.fakeHls.tech_;
  50. this.goalBufferLength =
  51. MasterPlaylistController.prototype.goalBufferLength.bind(this);
  52. this.mediaSource = new videojs.MediaSource();
  53. this.mediaSource.trigger('sourceopen');
  54. this.syncController = new SyncController();
  55. this.decrypter = worker(Decrypter, resolveDecrypterWorker());
  56. },
  57. afterEach(assert) {
  58. this.env.restore();
  59. this.mse.restore();
  60. this.decrypter.terminate();
  61. }
  62. };
  63. /**
  64. * Returns a settings object containing the custom settings provided merged with defaults
  65. * for use in constructing a segment loader. This function should be called with the QUnit
  66. * test environment the loader will be constructed in for proper this reference.
  67. *
  68. * @param {Object} settings
  69. * custom settings for the loader
  70. * @return {Object}
  71. * Settings object containing custom settings merged with defaults
  72. */
  73. export const LoaderCommonSettings = function(settings) {
  74. return videojs.mergeOptions({
  75. hls: this.fakeHls,
  76. currentTime: () => this.currentTime,
  77. seekable: () => this.seekable,
  78. seeking: () => this.seeking,
  79. hasPlayed: () => this.hasPlayed,
  80. duration: () => this.mediaSource.duration,
  81. goalBufferLength: () => this.goalBufferLength(),
  82. mediaSource: this.mediaSource,
  83. syncController: this.syncController,
  84. decrypter: this.decrypter
  85. }, settings);
  86. };
  87. /**
  88. * Sets up a QUnit module to run tests that should be run on all segment loader types.
  89. * Currently only two types, SegmentLoader and VTTSegmentLoader.
  90. *
  91. * @param {function(new:SegmentLoader|VTTLoader, Object)} LoaderConstructor
  92. * Constructor for segment loader. Takes one parameter, a settings object
  93. * @param {Object} loaderSettings
  94. * Custom settings to merge with defaults for the provided loader constructor
  95. * @param {function(SegmentLoader|VTTLoader)} loaderBeforeEach
  96. * Function to be run in the beforeEach after loader creation. Takes one parameter,
  97. * the loader for custom modifications to the loader object.
  98. */
  99. export const LoaderCommonFactory = (LoaderConstructor,
  100. loaderSettings,
  101. loaderBeforeEach) => {
  102. let loader;
  103. QUnit.module('Loader Common', function(hooks) {
  104. hooks.beforeEach(function(assert) {
  105. // Assume this module is nested and the parent module uses CommonHooks.beforeEach
  106. loader = new LoaderConstructor(LoaderCommonSettings.call(this, loaderSettings), {});
  107. loaderBeforeEach(loader);
  108. // shim updateend trigger to be a noop if the loader has no media source
  109. this.updateend = function() {
  110. if (loader.mediaSource_) {
  111. loader.mediaSource_.sourceBuffers[0].trigger('updateend');
  112. }
  113. };
  114. });
  115. QUnit.test('fails without required initialization options', function(assert) {
  116. /* eslint-disable no-new */
  117. assert.throws(function() {
  118. new LoaderConstructor();
  119. }, 'requires options');
  120. assert.throws(function() {
  121. new LoaderConstructor({});
  122. }, 'requires a currentTime callback');
  123. assert.throws(function() {
  124. new LoaderConstructor({
  125. currentTime() {}
  126. });
  127. }, 'requires a media source');
  128. /* eslint-enable */
  129. });
  130. QUnit.test('calling load is idempotent', function(assert) {
  131. loader.playlist(playlistWithDuration(20));
  132. loader.load();
  133. this.clock.tick(1);
  134. assert.equal(loader.state, 'WAITING', 'moves to the ready state');
  135. assert.equal(this.requests.length, 1, 'made one request');
  136. loader.load();
  137. assert.equal(loader.state, 'WAITING', 'still in the ready state');
  138. assert.equal(this.requests.length, 1, 'still one request');
  139. // some time passes and a response is received
  140. this.clock.tick(100);
  141. this.requests[0].response = new Uint8Array(10).buffer;
  142. this.requests.shift().respond(200, null, '');
  143. loader.load();
  144. assert.equal(this.requests.length, 0, 'load has no effect');
  145. // verify stats
  146. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  147. assert.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
  148. assert.equal(loader.mediaRequests, 1, '1 request');
  149. });
  150. QUnit.test('calling load should unpause', function(assert) {
  151. loader.playlist(playlistWithDuration(20));
  152. loader.pause();
  153. loader.load();
  154. this.clock.tick(1);
  155. assert.equal(loader.paused(), false, 'loading unpauses');
  156. loader.pause();
  157. this.clock.tick(1);
  158. this.requests[0].response = new Uint8Array(10).buffer;
  159. this.requests.shift().respond(200, null, '');
  160. assert.equal(loader.paused(), true, 'stayed paused');
  161. loader.load();
  162. assert.equal(loader.paused(), false, 'unpaused during processing');
  163. loader.pause();
  164. this.updateend();
  165. assert.equal(loader.state, 'READY', 'finished processing');
  166. assert.ok(loader.paused(), 'stayed paused');
  167. loader.load();
  168. assert.equal(loader.paused(), false, 'unpaused');
  169. // verify stats
  170. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  171. assert.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
  172. assert.equal(loader.mediaRequests, 1, '1 request');
  173. });
  174. QUnit.test('regularly checks the buffer while unpaused', function(assert) {
  175. loader.playlist(playlistWithDuration(90));
  176. loader.load();
  177. this.clock.tick(1);
  178. // fill the buffer
  179. this.clock.tick(1);
  180. this.requests[0].response = new Uint8Array(10).buffer;
  181. this.requests.shift().respond(200, null, '');
  182. loader.buffered_ = () => videojs.createTimeRanges([[
  183. 0, Config.GOAL_BUFFER_LENGTH
  184. ]]);
  185. this.updateend();
  186. assert.equal(this.requests.length, 0, 'no outstanding requests');
  187. // play some video to drain the buffer
  188. this.currentTime = Config.GOAL_BUFFER_LENGTH;
  189. this.clock.tick(10 * 1000);
  190. assert.equal(this.requests.length, 1, 'requested another segment');
  191. // verify stats
  192. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  193. assert.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
  194. assert.equal(loader.mediaRequests, 1, '1 request');
  195. });
  196. QUnit.test('does not check the buffer while paused', function(assert) {
  197. loader.playlist(playlistWithDuration(90));
  198. loader.load();
  199. this.clock.tick(1);
  200. loader.pause();
  201. this.clock.tick(1);
  202. this.requests[0].response = new Uint8Array(10).buffer;
  203. this.requests.shift().respond(200, null, '');
  204. this.updateend();
  205. this.clock.tick(10 * 1000);
  206. assert.equal(this.requests.length, 0, 'did not make a request');
  207. // verify stats
  208. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  209. assert.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
  210. assert.equal(loader.mediaRequests, 1, '1 request');
  211. });
  212. QUnit.test('calculates bandwidth after downloading a segment', function(assert) {
  213. loader.playlist(playlistWithDuration(10));
  214. loader.load();
  215. this.clock.tick(1);
  216. // some time passes and a response is received
  217. this.clock.tick(100);
  218. this.requests[0].response = new Uint8Array(10).buffer;
  219. this.requests.shift().respond(200, null, '');
  220. assert.equal(loader.bandwidth, (10 / 100) * 8 * 1000, 'calculated bandwidth');
  221. assert.equal(loader.roundTrip, 100, 'saves request round trip time');
  222. // TODO: Bandwidth Stat will be stale??
  223. // verify stats
  224. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  225. assert.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
  226. });
  227. QUnit.test('segment request timeouts reset bandwidth', function(assert) {
  228. loader.playlist(playlistWithDuration(10));
  229. loader.load();
  230. this.clock.tick(1);
  231. // a lot of time passes so the request times out
  232. this.requests[0].timedout = true;
  233. this.clock.tick(100 * 1000);
  234. assert.equal(loader.bandwidth, 1, 'reset bandwidth');
  235. assert.ok(isNaN(loader.roundTrip), 'reset round trip time');
  236. });
  237. QUnit.test('progress on segment requests are redispatched', function(assert) {
  238. let progressEvents = 0;
  239. loader.on('progress', function() {
  240. progressEvents++;
  241. });
  242. loader.playlist(playlistWithDuration(10));
  243. loader.load();
  244. this.clock.tick(1);
  245. this.requests[0].dispatchEvent({ type: 'progress', target: this.requests[0] });
  246. assert.equal(progressEvents, 1, 'triggered progress');
  247. });
  248. QUnit.test('aborts request at progress events if bandwidth is too low',
  249. function(assert) {
  250. const playlist1 = playlistWithDuration(10, { uri: 'playlist1.m3u8' });
  251. const playlist2 = playlistWithDuration(10, { uri: 'playlist2.m3u8' });
  252. const playlist3 = playlistWithDuration(10, { uri: 'playlist3.m3u8' });
  253. const playlist4 = playlistWithDuration(10, { uri: 'playlist4.m3u8' });
  254. const xhrOptions = {
  255. timeout: 15000
  256. };
  257. let bandwidthupdates = 0;
  258. let firstProgress = false;
  259. playlist1.attributes.BANDWIDTH = 18000;
  260. playlist2.attributes.BANDWIDTH = 10000;
  261. playlist3.attributes.BANDWIDTH = 8888;
  262. playlist4.attributes.BANDWIDTH = 7777;
  263. loader.hls_.playlists = {
  264. master: {
  265. playlists: [
  266. playlist1,
  267. playlist2,
  268. playlist3,
  269. playlist4
  270. ]
  271. }
  272. };
  273. const oldHandleProgress = loader.handleProgress_.bind(loader);
  274. loader.handleProgress_ = (event, simpleSegment) => {
  275. if (!firstProgress) {
  276. firstProgress = true;
  277. assert.equal(simpleSegment.stats.firstBytesReceivedAt, Date.now(),
  278. 'firstBytesReceivedAt timestamp added on first progress event with bytes');
  279. }
  280. oldHandleProgress(event, simpleSegment);
  281. };
  282. let earlyAborts = 0;
  283. loader.on('earlyabort', () => earlyAborts++);
  284. loader.on('bandwidthupdate', () => bandwidthupdates++);
  285. loader.playlist(playlist1, xhrOptions);
  286. loader.load();
  287. this.clock.tick(1);
  288. this.requests[0].dispatchEvent({
  289. type: 'progress',
  290. target: this.requests[0],
  291. loaded: 1
  292. });
  293. assert.equal(bandwidthupdates, 0, 'no bandwidth updates yet');
  294. assert.notOk(this.requests[0].aborted, 'request not prematurely aborted');
  295. assert.equal(earlyAborts, 0, 'no earlyabort events');
  296. this.clock.tick(999);
  297. this.requests[0].dispatchEvent({
  298. type: 'progress',
  299. target: this.requests[0],
  300. loaded: 2000
  301. });
  302. assert.equal(bandwidthupdates, 0, 'no bandwidth updates yet');
  303. assert.notOk(this.requests[0].aborted, 'request not prematurely aborted');
  304. assert.equal(earlyAborts, 0, 'no earlyabort events');
  305. this.clock.tick(2);
  306. this.requests[0].dispatchEvent({
  307. type: 'progress',
  308. target: this.requests[0],
  309. loaded: 2001
  310. });
  311. assert.equal(bandwidthupdates, 0, 'bandwidth not updated');
  312. assert.ok(this.requests[0].aborted, 'request aborted');
  313. assert.equal(earlyAborts, 1, 'earlyabort event triggered');
  314. });
  315. QUnit.test(
  316. 'appending a segment when loader is in walk-forward mode triggers bandwidthupdate',
  317. function(assert) {
  318. let progresses = 0;
  319. loader.on('bandwidthupdate', function() {
  320. progresses++;
  321. });
  322. loader.playlist(playlistWithDuration(20));
  323. loader.load();
  324. this.clock.tick(1);
  325. // some time passes and a response is received
  326. this.requests[0].response = new Uint8Array(10).buffer;
  327. this.requests.shift().respond(200, null, '');
  328. this.updateend();
  329. assert.equal(progresses, 0, 'no bandwidthupdate fired');
  330. this.clock.tick(2);
  331. // if mediaIndex is set, then the SegmentLoader is in walk-forward mode
  332. loader.mediaIndex = 1;
  333. // some time passes and a response is received
  334. this.requests[0].response = new Uint8Array(10).buffer;
  335. this.requests.shift().respond(200, null, '');
  336. this.updateend();
  337. assert.equal(progresses, 1, 'fired bandwidthupdate');
  338. // verify stats
  339. assert.equal(loader.mediaBytesTransferred, 20, '20 bytes');
  340. assert.equal(loader.mediaRequests, 2, '2 request');
  341. });
  342. QUnit.test('only requests one segment at a time', function(assert) {
  343. loader.playlist(playlistWithDuration(10));
  344. loader.load();
  345. this.clock.tick(1);
  346. // a bunch of time passes without recieving a response
  347. this.clock.tick(20 * 1000);
  348. assert.equal(this.requests.length, 1, 'only one request was made');
  349. });
  350. QUnit.test('downloads init segments if specified', function(assert) {
  351. let playlist = playlistWithDuration(20);
  352. let map = {
  353. resolvedUri: 'mainInitSegment',
  354. byterange: {
  355. length: 20,
  356. offset: 0
  357. }
  358. };
  359. let buffered = videojs.createTimeRanges();
  360. loader.buffered_ = () => buffered;
  361. playlist.segments[0].map = map;
  362. playlist.segments[1].map = map;
  363. loader.playlist(playlist);
  364. loader.load();
  365. this.clock.tick(1);
  366. assert.equal(this.requests.length, 2, 'made requests');
  367. // init segment response
  368. this.clock.tick(1);
  369. assert.equal(this.requests[0].url, 'mainInitSegment', 'requested the init segment');
  370. this.requests[0].response = new Uint8Array(20).buffer;
  371. this.requests.shift().respond(200, null, '');
  372. // 0.ts response
  373. this.clock.tick(1);
  374. assert.equal(this.requests[0].url, '0.ts',
  375. 'requested the segment');
  376. this.requests[0].response = new Uint8Array(20).buffer;
  377. this.requests.shift().respond(200, null, '');
  378. // append the init segment
  379. buffered = videojs.createTimeRanges([]);
  380. this.updateend();
  381. // append the segment
  382. buffered = videojs.createTimeRanges([[0, 10]]);
  383. this.updateend();
  384. this.clock.tick(1);
  385. assert.equal(this.requests.length, 1, 'made a request');
  386. assert.equal(this.requests[0].url, '1.ts',
  387. 'did not re-request the init segment');
  388. });
  389. QUnit.test('detects init segment changes and downloads it', function(assert) {
  390. let playlist = playlistWithDuration(20);
  391. let buffered = videojs.createTimeRanges();
  392. playlist.segments[0].map = {
  393. resolvedUri: 'init0',
  394. byterange: {
  395. length: 20,
  396. offset: 0
  397. }
  398. };
  399. playlist.segments[1].map = {
  400. resolvedUri: 'init0',
  401. byterange: {
  402. length: 20,
  403. offset: 20
  404. }
  405. };
  406. loader.buffered_ = () => buffered;
  407. loader.playlist(playlist);
  408. loader.load();
  409. this.clock.tick(1);
  410. assert.equal(this.requests.length, 2, 'made requests');
  411. // init segment response
  412. this.clock.tick(1);
  413. assert.equal(this.requests[0].url, 'init0', 'requested the init segment');
  414. assert.equal(this.requests[0].headers.Range, 'bytes=0-19',
  415. 'requested the init segment byte range');
  416. this.requests[0].response = new Uint8Array(20).buffer;
  417. this.requests.shift().respond(200, null, '');
  418. // 0.ts response
  419. this.clock.tick(1);
  420. assert.equal(this.requests[0].url, '0.ts',
  421. 'requested the segment');
  422. this.requests[0].response = new Uint8Array(20).buffer;
  423. this.requests.shift().respond(200, null, '');
  424. // append the init segment
  425. buffered = videojs.createTimeRanges([]);
  426. this.updateend();
  427. // append the segment
  428. buffered = videojs.createTimeRanges([[0, 10]]);
  429. this.updateend();
  430. this.clock.tick(1);
  431. assert.equal(this.requests.length, 2, 'made requests');
  432. assert.equal(this.requests[0].url, 'init0', 'requested the init segment');
  433. assert.equal(this.requests[0].headers.Range, 'bytes=20-39',
  434. 'requested the init segment byte range');
  435. assert.equal(this.requests[1].url, '1.ts',
  436. 'did not re-request the init segment');
  437. });
  438. QUnit.test('request error increments mediaRequestsErrored stat', function(assert) {
  439. loader.playlist(playlistWithDuration(20));
  440. loader.load();
  441. this.clock.tick(1);
  442. this.requests.shift().respond(404, null, '');
  443. // verify stats
  444. assert.equal(loader.mediaRequests, 1, '1 request');
  445. assert.equal(loader.mediaRequestsErrored, 1, '1 errored request');
  446. });
  447. QUnit.test('request timeout increments mediaRequestsTimedout stat', function(assert) {
  448. loader.playlist(playlistWithDuration(20));
  449. loader.load();
  450. this.clock.tick(1);
  451. this.requests[0].timedout = true;
  452. this.clock.tick(100 * 1000);
  453. // verify stats
  454. assert.equal(loader.mediaRequests, 1, '1 request');
  455. assert.equal(loader.mediaRequestsTimedout, 1, '1 timed-out request');
  456. });
  457. QUnit.test('request abort increments mediaRequestsAborted stat', function(assert) {
  458. loader.playlist(playlistWithDuration(20));
  459. loader.load();
  460. this.clock.tick(1);
  461. loader.abort();
  462. this.clock.tick(1);
  463. // verify stats
  464. assert.equal(loader.mediaRequests, 1, '1 request');
  465. assert.equal(loader.mediaRequestsAborted, 1, '1 aborted request');
  466. });
  467. QUnit.test('SegmentLoader.mediaIndex is adjusted when live playlist is updated',
  468. function(assert) {
  469. loader.playlist(playlistWithDuration(50, {
  470. mediaSequence: 0,
  471. endList: false
  472. }));
  473. loader.load();
  474. // Start at mediaIndex 2 which means that the next segment we request
  475. // should mediaIndex 3
  476. loader.mediaIndex = 2;
  477. this.clock.tick(1);
  478. assert.equal(loader.mediaIndex, 2, 'SegmentLoader.mediaIndex starts at 2');
  479. assert.equal(this.requests[0].url,
  480. '3.ts',
  481. 'requesting the segment at mediaIndex 3');
  482. this.requests[0].response = new Uint8Array(10).buffer;
  483. this.requests.shift().respond(200, null, '');
  484. this.clock.tick(1);
  485. this.updateend();
  486. assert.equal(loader.mediaIndex, 3, 'mediaIndex ends at 3');
  487. this.clock.tick(1);
  488. assert.equal(loader.mediaIndex, 3, 'SegmentLoader.mediaIndex starts at 3');
  489. assert.equal(this.requests[0].url,
  490. '4.ts',
  491. 'requesting the segment at mediaIndex 4');
  492. // Update the playlist shifting the mediaSequence by 2 which will result
  493. // in a decrement of the mediaIndex by 2 to 1
  494. loader.playlist(playlistWithDuration(50, {
  495. mediaSequence: 2,
  496. endList: false
  497. }));
  498. assert.equal(loader.mediaIndex, 1, 'SegmentLoader.mediaIndex is updated to 1');
  499. this.requests[0].response = new Uint8Array(10).buffer;
  500. this.requests.shift().respond(200, null, '');
  501. this.clock.tick(1);
  502. this.updateend();
  503. assert.equal(loader.mediaIndex, 2, 'SegmentLoader.mediaIndex ends at 2');
  504. });
  505. QUnit.test('segmentInfo.mediaIndex is adjusted when live playlist is updated',
  506. function(assert) {
  507. const handleUpdateEnd_ = loader.handleUpdateEnd_.bind(loader);
  508. let expectedLoaderIndex = 3;
  509. loader.handleUpdateEnd_ = function() {
  510. handleUpdateEnd_();
  511. assert.equal(loader.mediaIndex,
  512. expectedLoaderIndex,
  513. 'SegmentLoader.mediaIndex ends at' + expectedLoaderIndex);
  514. loader.mediaIndex = null;
  515. loader.fetchAtBuffer_ = false;
  516. // remove empty flag that may be added by vtt loader
  517. loader.playlist_.segments.forEach(segment => segment.empty = false);
  518. };
  519. // Setting currentTime to 31 so that we start requesting at segment #3
  520. this.currentTime = 31;
  521. loader.playlist(playlistWithDuration(50, {
  522. mediaSequence: 0,
  523. endList: false
  524. }));
  525. loader.load();
  526. // Start at mediaIndex null which means that the next segment we request
  527. // should be based on currentTime (mediaIndex 3)
  528. loader.mediaIndex = null;
  529. loader.syncPoint_ = {
  530. segmentIndex: 0,
  531. time: 0
  532. };
  533. this.clock.tick(1);
  534. let segmentInfo = loader.pendingSegment_;
  535. assert.equal(segmentInfo.mediaIndex, 3, 'segmentInfo.mediaIndex starts at 3');
  536. assert.equal(this.requests[0].url,
  537. '3.ts',
  538. 'requesting the segment at mediaIndex 3');
  539. this.requests[0].response = new Uint8Array(10).buffer;
  540. this.requests.shift().respond(200, null, '');
  541. this.clock.tick(1);
  542. this.updateend();
  543. this.clock.tick(1);
  544. segmentInfo = loader.pendingSegment_;
  545. assert.equal(segmentInfo.mediaIndex, 3, 'segmentInfo.mediaIndex starts at 3');
  546. assert.equal(this.requests[0].url,
  547. '3.ts',
  548. 'requesting the segment at mediaIndex 3');
  549. // Update the playlist shifting the mediaSequence by 2 which will result
  550. // in a decrement of the mediaIndex by 2 to 1
  551. loader.playlist(playlistWithDuration(50, {
  552. mediaSequence: 2,
  553. endList: false
  554. }));
  555. assert.equal(segmentInfo.mediaIndex, 1, 'segmentInfo.mediaIndex is updated to 1');
  556. expectedLoaderIndex = 1;
  557. this.requests[0].response = new Uint8Array(10).buffer;
  558. this.requests.shift().respond(200, null, '');
  559. this.clock.tick(1);
  560. this.updateend();
  561. });
  562. QUnit.test('segment 404s should trigger an error', function(assert) {
  563. let errors = [];
  564. loader.playlist(playlistWithDuration(10));
  565. loader.load();
  566. this.clock.tick(1);
  567. loader.on('error', function(error) {
  568. errors.push(error);
  569. });
  570. this.requests.shift().respond(404, null, '');
  571. assert.equal(errors.length, 1, 'triggered an error');
  572. assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
  573. assert.ok(loader.error().xhr, 'included the request object');
  574. assert.ok(loader.paused(), 'paused the loader');
  575. assert.equal(loader.state, 'READY', 'returned to the ready state');
  576. });
  577. QUnit.test('empty segments should trigger an error', function(assert) {
  578. let errors = [];
  579. loader.playlist(playlistWithDuration(10));
  580. loader.load();
  581. this.clock.tick(1);
  582. loader.on('error', function(error) {
  583. errors.push(error);
  584. });
  585. this.requests[0].response = new Uint8Array(0).buffer;
  586. this.requests.shift().respond(200, null, '');
  587. assert.equal(errors.length, 1, 'triggered an error');
  588. assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
  589. assert.ok(loader.error().xhr, 'included the request object');
  590. assert.ok(loader.paused(), 'paused the loader');
  591. assert.equal(loader.state, 'READY', 'returned to the ready state');
  592. });
  593. QUnit.test('segment 5xx status codes trigger an error', function(assert) {
  594. let errors = [];
  595. loader.playlist(playlistWithDuration(10));
  596. loader.load();
  597. this.clock.tick(1);
  598. loader.on('error', function(error) {
  599. errors.push(error);
  600. });
  601. this.requests.shift().respond(500, null, '');
  602. assert.equal(errors.length, 1, 'triggered an error');
  603. assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
  604. assert.ok(loader.error().xhr, 'included the request object');
  605. assert.ok(loader.paused(), 'paused the loader');
  606. assert.equal(loader.state, 'READY', 'returned to the ready state');
  607. });
  608. QUnit.test('remains ready if there are no segments', function(assert) {
  609. loader.playlist(playlistWithDuration(0));
  610. loader.load();
  611. this.clock.tick(1);
  612. assert.equal(loader.state, 'READY', 'in the ready state');
  613. });
  614. QUnit.test('dispose cleans up outstanding work', function(assert) {
  615. loader.playlist(playlistWithDuration(20));
  616. loader.load();
  617. this.clock.tick(1);
  618. loader.dispose();
  619. assert.ok(this.requests[0].aborted, 'aborted segment request');
  620. assert.equal(this.requests.length, 1, 'did not open another request');
  621. // Check that media source was properly cleaned up if it exists on the loader
  622. if (loader.mediaSource_) {
  623. loader.mediaSource_.sourceBuffers.forEach((sourceBuffer, i) => {
  624. let lastOperation = sourceBuffer.updates_.slice(-1)[0];
  625. assert.ok(lastOperation.abort, 'aborted source buffer ' + i);
  626. });
  627. }
  628. });
  629. // ----------
  630. // Decryption
  631. // ----------
  632. QUnit.test('calling load with an encrypted segment requests key and segment',
  633. function(assert) {
  634. assert.equal(loader.state, 'INIT', 'starts in the init state');
  635. loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
  636. assert.equal(loader.state, 'INIT', 'starts in the init state');
  637. assert.ok(loader.paused(), 'starts paused');
  638. loader.load();
  639. this.clock.tick(1);
  640. assert.equal(loader.state, 'WAITING', 'moves to the ready state');
  641. assert.ok(!loader.paused(), 'loading is not paused');
  642. assert.equal(this.requests.length, 2, 'requested a segment and key');
  643. assert.equal(this.requests[0].url,
  644. '0-key.php',
  645. 'requested the first segment\'s key');
  646. assert.equal(this.requests[1].url, '0.ts', 'requested the first segment');
  647. });
  648. QUnit.test('dispose cleans up key requests for encrypted segments', function(assert) {
  649. loader.playlist(playlistWithDuration(20, {isEncrypted: true}));
  650. loader.load();
  651. this.clock.tick(1);
  652. loader.dispose();
  653. assert.equal(this.requests.length, 2, 'requested a segment and key');
  654. assert.equal(this.requests[0].url,
  655. '0-key.php',
  656. 'requested the first segment\'s key');
  657. assert.ok(this.requests[0].aborted, 'aborted the first segment\s key request');
  658. assert.equal(this.requests.length, 2, 'did not open another request');
  659. });
  660. QUnit.test('key 404s should trigger an error', function(assert) {
  661. let errors = [];
  662. loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
  663. loader.load();
  664. this.clock.tick(1);
  665. loader.on('error', function(error) {
  666. errors.push(error);
  667. });
  668. this.requests.shift().respond(404, null, '');
  669. this.clock.tick(1);
  670. assert.equal(errors.length, 1, 'triggered an error');
  671. assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
  672. assert.equal(loader.error().message, 'HLS request errored at URL: 0-key.php',
  673. 'receieved a key error message');
  674. assert.ok(loader.error().xhr, 'included the request object');
  675. assert.ok(loader.paused(), 'paused the loader');
  676. assert.equal(loader.state, 'READY', 'returned to the ready state');
  677. });
  678. QUnit.test('key 5xx status codes trigger an error', function(assert) {
  679. let errors = [];
  680. loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
  681. loader.load();
  682. this.clock.tick(1);
  683. loader.on('error', function(error) {
  684. errors.push(error);
  685. });
  686. this.requests.shift().respond(500, null, '');
  687. assert.equal(errors.length, 1, 'triggered an error');
  688. assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
  689. assert.equal(loader.error().message, 'HLS request errored at URL: 0-key.php',
  690. 'receieved a key error message');
  691. assert.ok(loader.error().xhr, 'included the request object');
  692. assert.ok(loader.paused(), 'paused the loader');
  693. assert.equal(loader.state, 'READY', 'returned to the ready state');
  694. });
  695. QUnit.test('key request timeouts reset bandwidth', function(assert) {
  696. loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
  697. loader.load();
  698. this.clock.tick(1);
  699. assert.equal(this.requests[0].url,
  700. '0-key.php',
  701. 'requested the first segment\'s key');
  702. assert.equal(this.requests[1].url, '0.ts', 'requested the first segment');
  703. // a lot of time passes so the request times out
  704. this.requests[0].timedout = true;
  705. this.clock.tick(100 * 1000);
  706. assert.equal(loader.bandwidth, 1, 'reset bandwidth');
  707. assert.ok(isNaN(loader.roundTrip), 'reset round trip time');
  708. });
  709. QUnit.test('checks the goal buffer configuration every loading opportunity',
  710. function(assert) {
  711. let playlist = playlistWithDuration(20);
  712. let defaultGoal = Config.GOAL_BUFFER_LENGTH;
  713. let segmentInfo;
  714. Config.GOAL_BUFFER_LENGTH = 1;
  715. loader.playlist(playlist);
  716. loader.load();
  717. segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]),
  718. playlist,
  719. null,
  720. loader.hasPlayed_(),
  721. 0,
  722. null);
  723. assert.ok(!segmentInfo, 'no request generated');
  724. Config.GOAL_BUFFER_LENGTH = defaultGoal;
  725. });
  726. QUnit.test(
  727. 'does not skip over segment if live playlist update occurs while processing',
  728. function(assert) {
  729. let playlist = playlistWithDuration(40);
  730. let buffered = videojs.createTimeRanges();
  731. loader.buffered_ = () => buffered;
  732. playlist.endList = false;
  733. loader.playlist(playlist);
  734. loader.load();
  735. this.clock.tick(1);
  736. assert.equal(loader.pendingSegment_.uri, '0.ts', 'retrieving first segment');
  737. assert.equal(loader.pendingSegment_.segment.uri,
  738. '0.ts',
  739. 'correct segment reference');
  740. assert.equal(loader.state, 'WAITING', 'waiting for response');
  741. this.requests[0].response = new Uint8Array(10).buffer;
  742. this.requests.shift().respond(200, null, '');
  743. // playlist updated during append
  744. let playlistUpdated = playlistWithDuration(40);
  745. playlistUpdated.segments.shift();
  746. playlistUpdated.mediaSequence++;
  747. loader.playlist(playlistUpdated);
  748. // finish append
  749. buffered = videojs.createTimeRanges([[0, 10]]);
  750. this.updateend();
  751. this.clock.tick(1);
  752. assert.equal(loader.pendingSegment_.uri, '1.ts', 'retrieving second segment');
  753. assert.equal(loader.pendingSegment_.segment.uri,
  754. '1.ts',
  755. 'correct segment reference');
  756. assert.equal(loader.state, 'WAITING', 'waiting for response');
  757. });
  758. QUnit.test('processing segment reachable even after playlist update removes it',
  759. function(assert) {
  760. const handleUpdateEnd_ = loader.handleUpdateEnd_.bind(loader);
  761. let expectedURI = '0.ts';
  762. let playlist = playlistWithDuration(40);
  763. let buffered = videojs.createTimeRanges();
  764. loader.handleUpdateEnd_ = () => {
  765. // we need to check for the right state, as normally handleResponse would throw an
  766. // error under failing cases, but sinon swallows it as part of fake XML HTTP
  767. // request's response
  768. assert.equal(loader.state, 'APPENDING', 'moved to appending state');
  769. assert.equal(loader.pendingSegment_.uri, expectedURI, 'correct pending segment');
  770. assert.equal(loader.pendingSegment_.segment.uri,
  771. expectedURI,
  772. 'correct segment reference');
  773. handleUpdateEnd_();
  774. };
  775. loader.buffered_ = () => buffered;
  776. playlist.endList = false;
  777. loader.playlist(playlist);
  778. loader.load();
  779. this.clock.tick(1);
  780. assert.equal(loader.state, 'WAITING', 'in waiting state');
  781. assert.equal(loader.pendingSegment_.uri, '0.ts', 'first segment pending');
  782. assert.equal(loader.pendingSegment_.segment.uri,
  783. '0.ts',
  784. 'correct segment reference');
  785. // wrap up the first request to set mediaIndex and start normal live streaming
  786. this.requests[0].response = new Uint8Array(10).buffer;
  787. this.requests.shift().respond(200, null, '');
  788. buffered = videojs.createTimeRanges([[0, 10]]);
  789. this.updateend();
  790. this.clock.tick(1);
  791. assert.equal(loader.state, 'WAITING', 'in waiting state');
  792. assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment pending');
  793. assert.equal(loader.pendingSegment_.segment.uri,
  794. '1.ts',
  795. 'correct segment reference');
  796. // playlist updated during waiting
  797. let playlistUpdated = playlistWithDuration(40);
  798. playlistUpdated.segments.shift();
  799. playlistUpdated.segments.shift();
  800. playlistUpdated.mediaSequence += 2;
  801. loader.playlist(playlistUpdated);
  802. assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment still pending');
  803. assert.equal(loader.pendingSegment_.segment.uri,
  804. '1.ts',
  805. 'correct segment reference');
  806. expectedURI = '1.ts';
  807. this.requests[0].response = new Uint8Array(10).buffer;
  808. this.requests.shift().respond(200, null, '');
  809. this.updateend();
  810. });
  811. QUnit.test('new playlist always triggers syncinfoupdate', function(assert) {
  812. let playlist = playlistWithDuration(100, { endList: false });
  813. let syncInfoUpdates = 0;
  814. loader.on('syncinfoupdate', () => syncInfoUpdates++);
  815. loader.playlist(playlist);
  816. loader.load();
  817. assert.equal(syncInfoUpdates, 1, 'first playlist triggers an update');
  818. loader.playlist(playlist);
  819. assert.equal(syncInfoUpdates, 2, 'same playlist triggers an update');
  820. playlist = playlistWithDuration(100, { endList: false });
  821. loader.playlist(playlist);
  822. assert.equal(syncInfoUpdates, 3, 'new playlist with same info triggers an update');
  823. playlist.segments[0].start = 10;
  824. playlist = playlistWithDuration(100, { endList: false, mediaSequence: 1 });
  825. loader.playlist(playlist);
  826. assert.equal(syncInfoUpdates,
  827. 5,
  828. 'new playlist after expiring segment triggers two updates');
  829. });
  830. QUnit.module('Loading Calculation');
  831. QUnit.test('requests the first segment with an empty buffer', function(assert) {
  832. let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges(),
  833. playlistWithDuration(20),
  834. null,
  835. loader.hasPlayed_(),
  836. 0,
  837. null);
  838. assert.ok(segmentInfo, 'generated a request');
  839. assert.equal(segmentInfo.uri, '0.ts', 'requested the first segment');
  840. });
  841. QUnit.test('no request if video not played and 1 segment is buffered',
  842. function(assert) {
  843. this.hasPlayed = false;
  844. let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]),
  845. playlistWithDuration(20),
  846. 0,
  847. loader.hasPlayed_(),
  848. 0,
  849. null);
  850. assert.ok(!segmentInfo, 'no request generated');
  851. });
  852. QUnit.test('does not download the next segment if the buffer is full',
  853. function(assert) {
  854. let buffered;
  855. let segmentInfo;
  856. buffered = videojs.createTimeRanges([
  857. [0, 30 + Config.GOAL_BUFFER_LENGTH]
  858. ]);
  859. segmentInfo = loader.checkBuffer_(buffered,
  860. playlistWithDuration(30),
  861. null,
  862. true,
  863. 15,
  864. { segmentIndex: 0, time: 0 });
  865. assert.ok(!segmentInfo, 'no segment request generated');
  866. });
  867. QUnit.test('downloads the next segment if the buffer is getting low',
  868. function(assert) {
  869. let buffered;
  870. let segmentInfo;
  871. let playlist = playlistWithDuration(30);
  872. loader.playlist(playlist);
  873. buffered = videojs.createTimeRanges([[0, 19.999]]);
  874. segmentInfo = loader.checkBuffer_(buffered,
  875. playlist,
  876. 1,
  877. true,
  878. 15,
  879. { segmentIndex: 0, time: 0 });
  880. assert.ok(segmentInfo, 'made a request');
  881. assert.equal(segmentInfo.uri, '2.ts', 'requested the third segment');
  882. });
  883. QUnit.test('stops downloading segments at the end of the playlist', function(assert) {
  884. let buffered;
  885. let segmentInfo;
  886. buffered = videojs.createTimeRanges([[0, 60]]);
  887. segmentInfo = loader.checkBuffer_(buffered,
  888. playlistWithDuration(60),
  889. null,
  890. true,
  891. 0,
  892. null);
  893. assert.ok(!segmentInfo, 'no request was made');
  894. });
  895. QUnit.test('stops downloading segments if buffered past reported end of the playlist',
  896. function(assert) {
  897. let buffered;
  898. let segmentInfo;
  899. let playlist;
  900. buffered = videojs.createTimeRanges([[0, 59.9]]);
  901. playlist = playlistWithDuration(60);
  902. playlist.segments[playlist.segments.length - 1].end = 59.9;
  903. segmentInfo = loader.checkBuffer_(buffered,
  904. playlist,
  905. playlist.segments.length - 1,
  906. true,
  907. 50,
  908. { segmentIndex: 0, time: 0 });
  909. assert.ok(!segmentInfo, 'no request was made');
  910. });
  911. QUnit.test('doesn\'t allow more than one monitor buffer timer to be set',
  912. function(assert) {
  913. let timeoutCount = this.clock.methods.length;
  914. loader.monitorBuffer_();
  915. assert.equal(this.clock.methods.length,
  916. timeoutCount,
  917. 'timeout count remains the same');
  918. loader.monitorBuffer_();
  919. assert.equal(this.clock.methods.length,
  920. timeoutCount,
  921. 'timeout count remains the same');
  922. loader.monitorBuffer_();
  923. loader.monitorBuffer_();
  924. assert.equal(this.clock.methods.length,
  925. timeoutCount,
  926. 'timeout count remains the same');
  927. });
  928. });
  929. };