vtt-segment-loader.test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. import QUnit from 'qunit';
  2. import VTTSegmentLoader from '../src/vtt-segment-loader';
  3. import videojs from 'video.js';
  4. import {
  5. playlistWithDuration as oldPlaylistWithDuration,
  6. MockTextTrack
  7. } from './test-helpers.js';
  8. import {
  9. LoaderCommonHooks,
  10. LoaderCommonSettings,
  11. LoaderCommonFactory
  12. } from './loader-common.js';
  13. const oldVTT = window.WebVTT;
  14. const playlistWithDuration = function(time, conf) {
  15. return oldPlaylistWithDuration(time, videojs.mergeOptions({ extension: '.vtt' }, conf));
  16. };
  17. QUnit.module('VTTSegmentLoader', function(hooks) {
  18. hooks.beforeEach(function(assert) {
  19. LoaderCommonHooks.beforeEach.call(this);
  20. this.parserCreated = false;
  21. window.WebVTT = () => {};
  22. window.WebVTT.StringDecoder = () => {};
  23. window.WebVTT.Parser = () => {
  24. this.parserCreated = true;
  25. return {
  26. oncue() {},
  27. onparsingerror() {},
  28. onflush() {},
  29. parse() {},
  30. flush() {}
  31. };
  32. };
  33. // mock an initial timeline sync point on the SyncController
  34. this.syncController.timelines[0] = { time: 0, mapping: 0 };
  35. });
  36. hooks.afterEach(function(assert) {
  37. LoaderCommonHooks.afterEach.call(this);
  38. window.WebVTT = oldVTT;
  39. });
  40. LoaderCommonFactory(VTTSegmentLoader,
  41. { loaderType: 'vtt' },
  42. (loader) => loader.track(new MockTextTrack()));
  43. // Tests specific to the vtt loader go in this module
  44. QUnit.module('Loader VTT', function(nestedHooks) {
  45. let loader;
  46. nestedHooks.beforeEach(function(assert) {
  47. loader = new VTTSegmentLoader(LoaderCommonSettings.call(this, {
  48. loaderType: 'vtt'
  49. }), {});
  50. this.track = new MockTextTrack();
  51. });
  52. QUnit.test(`load waits until a playlist and track are specified to proceed`,
  53. function(assert) {
  54. loader.load();
  55. assert.equal(loader.state, 'INIT', 'waiting in init');
  56. assert.equal(loader.paused(), false, 'not paused');
  57. loader.playlist(playlistWithDuration(10));
  58. assert.equal(this.requests.length, 0, 'have not made a request yet');
  59. loader.track(this.track);
  60. this.clock.tick(1);
  61. assert.equal(this.requests.length, 1, 'made a request');
  62. assert.equal(loader.state, 'WAITING', 'transitioned states');
  63. });
  64. QUnit.test(`calling track and load begins buffering`, function(assert) {
  65. assert.equal(loader.state, 'INIT', 'starts in the init state');
  66. loader.playlist(playlistWithDuration(10));
  67. assert.equal(loader.state, 'INIT', 'starts in the init state');
  68. assert.ok(loader.paused(), 'starts paused');
  69. loader.track(this.track);
  70. assert.equal(loader.state, 'INIT', 'still in the init state');
  71. loader.load();
  72. this.clock.tick(1);
  73. assert.equal(loader.state, 'WAITING', 'moves to the ready state');
  74. assert.ok(!loader.paused(), 'loading is not paused');
  75. assert.equal(this.requests.length, 1, 'requested a segment');
  76. });
  77. QUnit.test('saves segment info to new segment after playlist refresh',
  78. function(assert) {
  79. let playlist = playlistWithDuration(40);
  80. let buffered = videojs.createTimeRanges();
  81. loader.buffered_ = () => buffered;
  82. playlist.endList = false;
  83. loader.playlist(playlist);
  84. loader.track(this.track);
  85. loader.load();
  86. this.clock.tick(1);
  87. assert.equal(loader.state, 'WAITING', 'in waiting state');
  88. assert.equal(loader.pendingSegment_.uri, '0.vtt', 'first segment pending');
  89. assert.equal(loader.pendingSegment_.segment.uri,
  90. '0.vtt',
  91. 'correct segment reference');
  92. // wrap up the first request to set mediaIndex and start normal live streaming
  93. this.requests[0].response = new Uint8Array(10).buffer;
  94. this.requests.shift().respond(200, null, '');
  95. buffered = videojs.createTimeRanges([[0, 10]]);
  96. this.clock.tick(1);
  97. assert.equal(loader.state, 'WAITING', 'in waiting state');
  98. assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment pending');
  99. assert.equal(loader.pendingSegment_.segment.uri,
  100. '1.vtt',
  101. 'correct segment reference');
  102. // playlist updated during waiting
  103. let playlistUpdated = playlistWithDuration(40);
  104. playlistUpdated.segments.shift();
  105. playlistUpdated.mediaSequence++;
  106. loader.playlist(playlistUpdated);
  107. assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment still pending');
  108. assert.equal(loader.pendingSegment_.segment.uri,
  109. '1.vtt',
  110. 'correct segment reference');
  111. // mock parseVttCues_ to respond empty cue array
  112. loader.parseVTTCues_ = (segmentInfo) => {
  113. segmentInfo.cues = [];
  114. segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
  115. };
  116. this.requests[0].response = new Uint8Array(10).buffer;
  117. this.requests.shift().respond(200, null, '');
  118. assert.ok(playlistUpdated.segments[0].empty,
  119. 'set empty on segment of new playlist');
  120. assert.ok(!playlist.segments[1].empty,
  121. 'did not set empty on segment of old playlist');
  122. });
  123. QUnit.test(
  124. 'saves segment info to old segment after playlist refresh if segment fell off',
  125. function(assert) {
  126. let playlist = playlistWithDuration(40);
  127. let buffered = videojs.createTimeRanges();
  128. loader.buffered_ = () => buffered;
  129. playlist.endList = false;
  130. loader.playlist(playlist);
  131. loader.track(this.track);
  132. loader.load();
  133. this.clock.tick(1);
  134. assert.equal(loader.state, 'WAITING', 'in waiting state');
  135. assert.equal(loader.pendingSegment_.uri, '0.vtt', 'first segment pending');
  136. assert.equal(loader.pendingSegment_.segment.uri,
  137. '0.vtt',
  138. 'correct segment reference');
  139. // wrap up the first request to set mediaIndex and start normal live streaming
  140. this.requests[0].response = new Uint8Array(10).buffer;
  141. this.requests.shift().respond(200, null, '');
  142. buffered = videojs.createTimeRanges([[0, 10]]);
  143. this.clock.tick(1);
  144. assert.equal(loader.state, 'WAITING', 'in waiting state');
  145. assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment pending');
  146. assert.equal(loader.pendingSegment_.segment.uri,
  147. '1.vtt',
  148. 'correct segment reference');
  149. // playlist updated during waiting
  150. let playlistUpdated = playlistWithDuration(40);
  151. playlistUpdated.segments.shift();
  152. playlistUpdated.segments.shift();
  153. playlistUpdated.mediaSequence += 2;
  154. loader.playlist(playlistUpdated);
  155. assert.equal(loader.pendingSegment_.uri, '1.vtt', 'second segment still pending');
  156. assert.equal(loader.pendingSegment_.segment.uri,
  157. '1.vtt',
  158. 'correct segment reference');
  159. // mock parseVttCues_ to respond empty cue array
  160. loader.parseVTTCues_ = (segmentInfo) => {
  161. segmentInfo.cues = [];
  162. segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
  163. };
  164. this.requests[0].response = new Uint8Array(10).buffer;
  165. this.requests.shift().respond(200, null, '');
  166. assert.ok(playlist.segments[1].empty,
  167. 'set empty on segment of old playlist');
  168. assert.ok(!playlistUpdated.segments[0].empty,
  169. 'no empty info for first segment of new playlist');
  170. });
  171. QUnit.test('waits for syncController to have sync info for the timeline of the vtt' +
  172. 'segment being requested before loading', function(assert) {
  173. let playlist = playlistWithDuration(40);
  174. let loadedSegment = false;
  175. loader.loadSegment_ = () => {
  176. loader.state = 'WAITING';
  177. loadedSegment = true;
  178. };
  179. loader.checkBuffer_ = () => {
  180. return { mediaIndex: 2, timeline: 2, segment: { } };
  181. };
  182. loader.playlist(playlist);
  183. loader.track(this.track);
  184. loader.load();
  185. assert.equal(loader.state, 'READY', 'loader is ready at start');
  186. assert.ok(!loadedSegment, 'no segment requests made yet');
  187. this.clock.tick(1);
  188. assert.equal(loader.state,
  189. 'WAITING_ON_TIMELINE',
  190. 'loader waiting for timeline info');
  191. assert.ok(!loadedSegment, 'no segment requests made yet');
  192. // simulate the main segment loader finding timeline info for the new timeline
  193. loader.syncController_.timelines[2] = { time: 20, mapping: -10 };
  194. loader.syncController_.trigger('timestampoffset');
  195. assert.equal(loader.state,
  196. 'READY',
  197. 'ready after sync controller reports timeline info');
  198. assert.ok(!loadedSegment, 'no segment requests made yet');
  199. this.clock.tick(1);
  200. assert.equal(loader.state, 'WAITING', 'loader waiting on segment request');
  201. assert.ok(loadedSegment, 'made call to load segment on new timeline');
  202. });
  203. QUnit.test('waits for vtt.js to be loaded before attempting to parse cues',
  204. function(assert) {
  205. const vttjs = window.WebVTT;
  206. let playlist = playlistWithDuration(40);
  207. let parsedCues = false;
  208. delete window.WebVTT;
  209. loader.handleUpdateEnd_ = () => {
  210. parsedCues = true;
  211. loader.state = 'READY';
  212. };
  213. let vttjsCallback = () => {};
  214. this.track.tech_ = {
  215. one(event, callback) {
  216. if (event === 'vttjsloaded') {
  217. vttjsCallback = callback;
  218. }
  219. },
  220. trigger(event) {
  221. if (event === 'vttjsloaded') {
  222. vttjsCallback();
  223. }
  224. },
  225. off() {}
  226. };
  227. loader.playlist(playlist);
  228. loader.track(this.track);
  229. loader.load();
  230. assert.equal(loader.state, 'READY', 'loader is ready at start');
  231. assert.ok(!parsedCues, 'no cues parsed yet');
  232. this.clock.tick(1);
  233. assert.equal(loader.state, 'WAITING', 'loader is waiting on segment request');
  234. assert.ok(!parsedCues, 'no cues parsed yet');
  235. this.requests[0].response = new Uint8Array(10).buffer;
  236. this.requests.shift().respond(200, null, '');
  237. this.clock.tick(1);
  238. assert.equal(loader.state,
  239. 'WAITING_ON_VTTJS',
  240. 'loader is waiting for vttjs to be loaded');
  241. assert.ok(!parsedCues, 'no cues parsed yet');
  242. window.WebVTT = vttjs;
  243. loader.subtitlesTrack_.tech_.trigger('vttjsloaded');
  244. assert.equal(loader.state, 'READY', 'loader is ready to load next segment');
  245. assert.ok(parsedCues, 'parsed cues');
  246. });
  247. QUnit.test('uses timestampmap from vtt header to set cue and segment timing',
  248. function(assert) {
  249. const cues = [
  250. { startTime: 10, endTime: 12 },
  251. { startTime: 14, endTime: 16 },
  252. { startTime: 15, endTime: 19 }
  253. ];
  254. const expectedCueTimes = [
  255. { startTime: 14, endTime: 16 },
  256. { startTime: 18, endTime: 20 },
  257. { startTime: 19, endTime: 23 }
  258. ];
  259. const expectedSegment = {
  260. duration: 10
  261. };
  262. const expectedPlaylist = {
  263. mediaSequence: 100,
  264. syncInfo: { mediaSequence: 102, time: 9 }
  265. };
  266. const mappingObj = {
  267. time: 0,
  268. mapping: -10
  269. };
  270. const playlist = { mediaSequence: 100 };
  271. const segment = { duration: 10 };
  272. const segmentInfo = {
  273. timestampmap: { MPEGTS: 1260000, LOCAL: 0 },
  274. mediaIndex: 2,
  275. cues,
  276. segment
  277. };
  278. loader.updateTimeMapping_(segmentInfo, mappingObj, playlist);
  279. assert.deepEqual(cues,
  280. expectedCueTimes,
  281. 'adjusted cue timing based on timestampmap');
  282. assert.deepEqual(segment,
  283. expectedSegment,
  284. 'set segment start and end based on cue content');
  285. assert.deepEqual(playlist,
  286. expectedPlaylist,
  287. 'set syncInfo for playlist based on learned segment start');
  288. });
  289. QUnit.test('loader logs vtt.js ParsingErrors and does not trigger an error event',
  290. function(assert) {
  291. let playlist = playlistWithDuration(40);
  292. window.WebVTT.Parser = () => {
  293. this.parserCreated = true;
  294. return {
  295. oncue() {},
  296. onparsingerror() {},
  297. onflush() {},
  298. parse() {
  299. // MOCK parsing the cues below
  300. this.onparsingerror({ message: 'BAD CUE'});
  301. this.oncue({ startTime: 5, endTime: 6});
  302. this.onparsingerror({ message: 'BAD --> CUE' });
  303. },
  304. flush() {}
  305. };
  306. };
  307. loader.playlist(playlist);
  308. loader.track(this.track);
  309. loader.load();
  310. this.clock.tick(1);
  311. const vttString = `
  312. WEBVTT
  313. 00:00:03.000 -> 00:00:05.000
  314. <i>BAD CUE</i>
  315. 00:00:05.000 --> 00:00:06.000
  316. <b>GOOD CUE</b>
  317. 00:00:07.000 --> 00:00:10.000
  318. <i>BAD --> CUE</i>
  319. `;
  320. // state WAITING for segment response
  321. this.requests[0].response =
  322. new Uint8Array(vttString.split('').map(char => char.charCodeAt(0)));
  323. this.requests.shift().respond(200, null, '');
  324. this.clock.tick(1);
  325. assert.equal(loader.subtitlesTrack_.cues.length,
  326. 1,
  327. 'only appended the one good cue');
  328. assert.equal(this.env.log.warn.callCount,
  329. 2,
  330. 'logged two warnings, one for each invalid cue');
  331. this.env.log.warn.callCount = 0;
  332. });
  333. QUnit.test('Cues that overlap segment boundaries',
  334. function(assert) {
  335. let playlist = playlistWithDuration(20);
  336. loader.parseVTTCues_ = (segmentInfo) => {
  337. segmentInfo.cues = [{ startTime: 0, endTime: 5}, { startTime: 5, endTime: 15}];
  338. segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
  339. };
  340. loader.playlist(playlist);
  341. loader.track(this.track);
  342. loader.load();
  343. this.clock.tick(1);
  344. this.requests[0].response = new Uint8Array(10).buffer;
  345. this.requests.shift().respond(200, null, '');
  346. this.clock.tick(1);
  347. assert.equal(this.track.cues.length, 2, 'segment length should be 2');
  348. loader.parseVTTCues_ = (segmentInfo) => {
  349. segmentInfo.cues = [{ startTime: 5, endTime: 15}, { startTime: 15, endTime: 20}];
  350. segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
  351. };
  352. this.clock.tick(1);
  353. this.requests[0].response = new Uint8Array(10).buffer;
  354. this.requests.shift().respond(200, null, '');
  355. this.clock.tick(1);
  356. assert.equal(this.track.cues.length, 3, 'segment length should be 3');
  357. assert.equal(this.track.cues[0].startTime, 0, 'First cue starttime should be 0');
  358. assert.equal(this.track.cues[1].startTime, 5, 'Second cue starttime should be 5');
  359. assert.equal(this.track.cues[2].startTime, 15, 'Third cue starttime should be 15');
  360. });
  361. QUnit.test('loader does not re-request segments that contain no subtitles',
  362. function(assert) {
  363. let playlist = playlistWithDuration(60);
  364. playlist.endList = false;
  365. loader.parseVTTCues_ = (segmentInfo) => {
  366. // mock empty segment
  367. segmentInfo.cues = [];
  368. };
  369. loader.currentTime_ = () => {
  370. return 30;
  371. };
  372. loader.playlist(playlist);
  373. loader.track(this.track);
  374. loader.load();
  375. this.clock.tick(1);
  376. assert.equal(loader.pendingSegment_.mediaIndex,
  377. 2,
  378. 'requesting initial segment guess');
  379. this.requests[0].response = new Uint8Array(10).buffer;
  380. this.requests.shift().respond(200, null, '');
  381. this.clock.tick(1);
  382. assert.ok(playlist.segments[2].empty, 'marked empty segment as empty');
  383. assert.equal(loader.pendingSegment_.mediaIndex,
  384. 3,
  385. 'walked forward skipping requesting empty segment');
  386. });
  387. QUnit.test('loader triggers error event on fatal vtt.js errors', function(assert) {
  388. let playlist = playlistWithDuration(40);
  389. let errors = 0;
  390. loader.parseVTTCues_ = () => {
  391. throw new Error('fatal error');
  392. };
  393. loader.on('error', () => errors++);
  394. loader.playlist(playlist);
  395. loader.track(this.track);
  396. loader.load();
  397. assert.equal(errors, 0, 'no error at loader start');
  398. this.clock.tick(1);
  399. // state WAITING for segment response
  400. this.requests[0].response = new Uint8Array(10).buffer;
  401. this.requests.shift().respond(200, null, '');
  402. this.clock.tick(1);
  403. assert.equal(errors, 1, 'triggered error when parser emmitts fatal error');
  404. assert.ok(loader.paused(), 'loader paused when encountering fatal error');
  405. assert.equal(loader.state, 'READY', 'loader reset after error');
  406. });
  407. QUnit.test('loader triggers error event when vtt.js fails to load', function(assert) {
  408. let playlist = playlistWithDuration(40);
  409. let errors = 0;
  410. delete window.WebVTT;
  411. let vttjsCallback = () => {};
  412. this.track.tech_ = {
  413. one(event, callback) {
  414. if (event === 'vttjserror') {
  415. vttjsCallback = callback;
  416. }
  417. },
  418. trigger(event) {
  419. if (event === 'vttjserror') {
  420. vttjsCallback();
  421. }
  422. },
  423. off() {}
  424. };
  425. loader.on('error', () => errors++);
  426. loader.playlist(playlist);
  427. loader.track(this.track);
  428. loader.load();
  429. assert.equal(loader.state, 'READY', 'loader is ready at start');
  430. assert.equal(errors, 0, 'no errors yet');
  431. this.clock.tick(1);
  432. assert.equal(loader.state, 'WAITING', 'loader is waiting on segment request');
  433. assert.equal(errors, 0, 'no errors yet');
  434. this.requests[0].response = new Uint8Array(10).buffer;
  435. this.requests.shift().respond(200, null, '');
  436. this.clock.tick(1);
  437. assert.equal(loader.state,
  438. 'WAITING_ON_VTTJS',
  439. 'loader is waiting for vttjs to be loaded');
  440. assert.equal(errors, 0, 'no errors yet');
  441. loader.subtitlesTrack_.tech_.trigger('vttjserror');
  442. assert.equal(loader.state, 'READY', 'loader is reset to ready');
  443. assert.ok(loader.paused(), 'loader is paused after error');
  444. assert.equal(errors, 1, 'loader triggered error when vtt.js load triggers error');
  445. });
  446. });
  447. });