segment-loader.test.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. import QUnit from 'qunit';
  2. import {
  3. default as SegmentLoader,
  4. illegalMediaSwitch,
  5. safeBackBufferTrimTime
  6. } from '../src/segment-loader';
  7. import videojs from 'video.js';
  8. import mp4probe from 'mux.js/lib/mp4/probe';
  9. import {
  10. playlistWithDuration,
  11. MockTextTrack
  12. } from './test-helpers.js';
  13. import {
  14. LoaderCommonHooks,
  15. LoaderCommonSettings,
  16. LoaderCommonFactory
  17. } from './loader-common.js';
  18. import sinon from 'sinon';
  19. // noop addSegmentMetadataCue_ since most test segments dont have real timing information
  20. // save the original function to a variable to patch it back in for the metadata cue
  21. // specific tests
  22. const ogAddSegmentMetadataCue_ = SegmentLoader.prototype.addSegmentMetadataCue_;
  23. SegmentLoader.prototype.addSegmentMetadataCue_ = function() {};
  24. QUnit.module('SegmentLoader Isolated Functions');
  25. QUnit.test('illegalMediaSwitch detects illegal media switches', function(assert) {
  26. let startingMedia = { containsAudio: true, containsVideo: true };
  27. let newSegmentMedia = { containsAudio: true, containsVideo: true };
  28. assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  29. 'no error when muxed to muxed');
  30. startingMedia = { containsAudio: true, containsVideo: true };
  31. newSegmentMedia = { containsAudio: false, containsVideo: false };
  32. assert.notOk(illegalMediaSwitch('audio', startingMedia, newSegmentMedia),
  33. 'no error when not main loader type');
  34. startingMedia = { containsAudio: true, containsVideo: false };
  35. newSegmentMedia = { containsAudio: true, containsVideo: false };
  36. assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  37. 'no error when audio only to audio only');
  38. startingMedia = { containsAudio: false, containsVideo: true };
  39. newSegmentMedia = { containsAudio: false, containsVideo: true };
  40. assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  41. 'no error when video only to video only');
  42. startingMedia = { containsAudio: false, containsVideo: true };
  43. newSegmentMedia = { containsAudio: true, containsVideo: true };
  44. assert.notOk(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  45. 'no error when video only to muxed');
  46. startingMedia = { containsAudio: true, containsVideo: true };
  47. newSegmentMedia = { containsAudio: false, containsVideo: false };
  48. assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  49. 'Neither audio nor video found in segment.',
  50. 'error when neither audio nor video');
  51. startingMedia = { containsAudio: true, containsVideo: false };
  52. newSegmentMedia = { containsAudio: false, containsVideo: false };
  53. assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  54. 'Neither audio nor video found in segment.',
  55. 'error when audio only to neither audio nor video');
  56. startingMedia = { containsAudio: false, containsVideo: true };
  57. newSegmentMedia = { containsAudio: false, containsVideo: false };
  58. assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  59. 'Neither audio nor video found in segment.',
  60. 'error when video only to neither audio nor video');
  61. startingMedia = { containsAudio: true, containsVideo: false };
  62. newSegmentMedia = { containsAudio: true, containsVideo: true };
  63. assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  64. 'Video found in segment when we expected only audio.' +
  65. ' We can\'t switch to a stream with video from an audio only stream.' +
  66. ' To get rid of this message, please add codec information to the' +
  67. ' manifest.',
  68. 'error when audio only to muxed');
  69. startingMedia = { containsAudio: true, containsVideo: true };
  70. newSegmentMedia = { containsAudio: true, containsVideo: false };
  71. assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  72. 'Only audio found in segment when we expected video.' +
  73. ' We can\'t switch to audio only from a stream that had video.' +
  74. ' To get rid of this message, please add codec information to the' +
  75. ' manifest.',
  76. 'error when muxed to audio only');
  77. startingMedia = { containsAudio: true, containsVideo: false };
  78. newSegmentMedia = { containsAudio: false, containsVideo: true };
  79. assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  80. 'Video found in segment when we expected only audio.' +
  81. ' We can\'t switch to a stream with video from an audio only stream.' +
  82. ' To get rid of this message, please add codec information to the' +
  83. ' manifest.',
  84. 'error when audio only to video only');
  85. startingMedia = { containsAudio: false, containsVideo: true };
  86. newSegmentMedia = { containsAudio: true, containsVideo: false };
  87. assert.equal(illegalMediaSwitch('main', startingMedia, newSegmentMedia),
  88. 'Only audio found in segment when we expected video.' +
  89. ' We can\'t switch to audio only from a stream that had video.' +
  90. ' To get rid of this message, please add codec information to the' +
  91. ' manifest.',
  92. 'error when video only to audio only');
  93. });
  94. QUnit.test('safeBackBufferTrimTime determines correct safe removeToTime',
  95. function(assert) {
  96. let seekable = videojs.createTimeRanges([[75, 120]]);
  97. let targetDuration = 10;
  98. let currentTime = 70;
  99. assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 40,
  100. 'uses 30s before current time if currentTime is before seekable start');
  101. currentTime = 110;
  102. assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 75,
  103. 'uses seekable start if currentTime is after seekable start');
  104. currentTime = 80;
  105. assert.equal(safeBackBufferTrimTime(seekable, currentTime, targetDuration), 70,
  106. 'uses target duration before currentTime if currentTime is after seekable but' +
  107. 'within target duration');
  108. });
  109. QUnit.module('SegmentLoader', function(hooks) {
  110. hooks.beforeEach(LoaderCommonHooks.beforeEach);
  111. hooks.afterEach(LoaderCommonHooks.afterEach);
  112. LoaderCommonFactory(SegmentLoader,
  113. { loaderType: 'main' },
  114. (loader) => loader.mimeType('video/mp2t'));
  115. // Tests specific to the main segment loader go in this module
  116. QUnit.module('Loader Main', function(nestedHooks) {
  117. let loader;
  118. nestedHooks.beforeEach(function(assert) {
  119. this.segmentMetadataTrack = new MockTextTrack();
  120. this.startTime = sinon.stub(mp4probe, 'startTime');
  121. this.mimeType = 'video/mp2t';
  122. loader = new SegmentLoader(LoaderCommonSettings.call(this, {
  123. loaderType: 'main',
  124. segmentMetadataTrack: this.segmentMetadataTrack
  125. }), {});
  126. // shim updateend trigger to be a noop if the loader has no media source
  127. this.updateend = function() {
  128. if (loader.mediaSource_) {
  129. loader.mediaSource_.sourceBuffers[0].trigger('updateend');
  130. }
  131. };
  132. });
  133. nestedHooks.afterEach(function(assert) {
  134. this.startTime.restore();
  135. });
  136. QUnit.test(`load waits until a playlist and mime type are specified to proceed`,
  137. function(assert) {
  138. loader.load();
  139. assert.equal(loader.state, 'INIT', 'waiting in init');
  140. assert.equal(loader.paused(), false, 'not paused');
  141. loader.playlist(playlistWithDuration(10));
  142. assert.equal(this.requests.length, 0, 'have not made a request yet');
  143. loader.mimeType(this.mimeType);
  144. this.clock.tick(1);
  145. assert.equal(this.requests.length, 1, 'made a request');
  146. assert.equal(loader.state, 'WAITING', 'transitioned states');
  147. });
  148. QUnit.test(`calling mime type and load begins buffering`, function(assert) {
  149. assert.equal(loader.state, 'INIT', 'starts in the init state');
  150. loader.playlist(playlistWithDuration(10));
  151. assert.equal(loader.state, 'INIT', 'starts in the init state');
  152. assert.ok(loader.paused(), 'starts paused');
  153. loader.mimeType(this.mimeType);
  154. assert.equal(loader.state, 'INIT', 'still in the init state');
  155. loader.load();
  156. this.clock.tick(1);
  157. assert.equal(loader.state, 'WAITING', 'moves to the ready state');
  158. assert.ok(!loader.paused(), 'loading is not paused');
  159. assert.equal(this.requests.length, 1, 'requested a segment');
  160. });
  161. QUnit.test('only appends one segment at a time', function(assert) {
  162. loader.playlist(playlistWithDuration(10));
  163. loader.mimeType(this.mimeType);
  164. loader.load();
  165. this.clock.tick(1);
  166. // some time passes and a segment is received
  167. this.clock.tick(100);
  168. this.requests[0].response = new Uint8Array(10).buffer;
  169. this.requests.shift().respond(200, null, '');
  170. // a lot of time goes by without "updateend"
  171. this.clock.tick(20 * 1000);
  172. assert.equal(this.mediaSource.sourceBuffers[0].updates_.filter(
  173. update => update.append).length, 1, 'only one append');
  174. assert.equal(this.requests.length, 0, 'only made one request');
  175. // verify stats
  176. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  177. assert.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
  178. assert.equal(loader.mediaRequests, 1, '1 request');
  179. });
  180. QUnit.test('updates timestamps when segments do not start at zero', function(assert) {
  181. let playlist = playlistWithDuration(10);
  182. playlist.segments.forEach((segment) => {
  183. segment.map = {
  184. resolvedUri: 'init.mp4',
  185. byterange: { length: Infinity, offset: 0 }
  186. };
  187. });
  188. loader.playlist(playlist);
  189. loader.mimeType(this.mimeType);
  190. loader.load();
  191. this.startTime.returns(11);
  192. this.clock.tick(100);
  193. // init
  194. this.requests[0].response = new Uint8Array(10).buffer;
  195. this.requests.shift().respond(200, null, '');
  196. // segment
  197. this.requests[0].response = new Uint8Array(10).buffer;
  198. this.requests.shift().respond(200, null, '');
  199. assert.equal(loader.sourceUpdater_.timestampOffset(), -11, 'set timestampOffset');
  200. assert.equal(playlist.segments[0].start,
  201. 0,
  202. 'segment start time not shifted by mp4 start time');
  203. assert.equal(playlist.segments[0].end,
  204. 10,
  205. 'segment end time not shifted by mp4 start time');
  206. });
  207. QUnit.test('triggers syncinfoupdate before attempting a resync', function(assert) {
  208. let syncInfoUpdates = 0;
  209. loader.playlist(playlistWithDuration(20));
  210. loader.mimeType(this.mimeType);
  211. loader.load();
  212. this.clock.tick(1);
  213. this.seekable = videojs.createTimeRanges([[0, 10]]);
  214. this.syncController.probeSegmentInfo = (segmentInfo) => {
  215. let segment = segmentInfo.segment;
  216. segment.end = 10;
  217. };
  218. loader.on('syncinfoupdate', () => {
  219. syncInfoUpdates++;
  220. // Simulate the seekable window updating
  221. this.seekable = videojs.createTimeRanges([[200, 210]]);
  222. // Simulate the seek to live that should happen in playback-watcher
  223. this.currentTime = 210;
  224. });
  225. this.requests[0].response = new Uint8Array(10).buffer;
  226. this.requests.shift().respond(200, null, '');
  227. this.updateend();
  228. this.clock.tick(1);
  229. assert.equal(loader.mediaIndex, null, 'mediaIndex reset by seek to seekable');
  230. assert.equal(syncInfoUpdates, 1, 'syncinfoupdate was triggered');
  231. });
  232. QUnit.test('abort does not cancel segment processing in progress', function(assert) {
  233. loader.playlist(playlistWithDuration(20));
  234. loader.mimeType(this.mimeType);
  235. loader.load();
  236. this.clock.tick(1);
  237. this.requests[0].response = new Uint8Array(10).buffer;
  238. this.requests.shift().respond(200, null, '');
  239. loader.abort();
  240. this.clock.tick(1);
  241. assert.equal(loader.state, 'APPENDING', 'still appending');
  242. // verify stats
  243. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  244. assert.equal(loader.mediaRequests, 1, '1 request');
  245. });
  246. QUnit.test('sets the timestampOffset on timeline change', function(assert) {
  247. let playlist = playlistWithDuration(40);
  248. let buffered = videojs.createTimeRanges();
  249. let hlsTimestampOffsetEvents = 0;
  250. loader.on('timestampoffset', () => {
  251. hlsTimestampOffsetEvents++;
  252. });
  253. loader.buffered_ = () => buffered;
  254. playlist.discontinuityStarts = [1];
  255. playlist.segments[1].timeline = 1;
  256. loader.playlist(playlist);
  257. loader.mimeType(this.mimeType);
  258. loader.load();
  259. this.clock.tick(1);
  260. // segment 0
  261. this.requests[0].response = new Uint8Array(10).buffer;
  262. this.requests.shift().respond(200, null, '');
  263. buffered = videojs.createTimeRanges([[0, 10]]);
  264. this.updateend();
  265. this.clock.tick(1);
  266. assert.equal(hlsTimestampOffsetEvents, 0,
  267. 'no hls-timestamp-offset event was fired');
  268. // segment 1, discontinuity
  269. this.requests[0].response = new Uint8Array(10).buffer;
  270. this.requests.shift().respond(200, null, '');
  271. assert.equal(loader.mediaSource_.sourceBuffers[0].timestampOffset,
  272. 10,
  273. 'set timestampOffset');
  274. // verify stats
  275. assert.equal(loader.mediaBytesTransferred, 20, '20 bytes');
  276. assert.equal(loader.mediaRequests, 2, '2 requests');
  277. assert.equal(hlsTimestampOffsetEvents, 1,
  278. 'an hls-timestamp-offset event was fired');
  279. });
  280. QUnit.test('tracks segment end times as they are buffered', function(assert) {
  281. let playlist = playlistWithDuration(20);
  282. loader.syncController_.probeTsSegment_ = function(segmentInfo) {
  283. return { start: 0, end: 9.5 };
  284. };
  285. loader.playlist(playlist);
  286. loader.mimeType(this.mimeType);
  287. loader.load();
  288. this.clock.tick(1);
  289. this.requests[0].response = new Uint8Array(10).buffer;
  290. this.requests.shift().respond(200, null, '');
  291. this.updateend();
  292. this.clock.tick(1);
  293. assert.equal(playlist.segments[0].end, 9.5, 'updated duration');
  294. // verify stats
  295. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  296. assert.equal(loader.mediaRequests, 1, '1 request');
  297. });
  298. QUnit.test('loader triggers segmenttimemapping before appending segment',
  299. function(assert) {
  300. let playlist = playlistWithDuration(20);
  301. let segmenttimemappings = 0;
  302. let timingInfo = { hasMapping: false };
  303. this.syncController.probeSegmentInfo = () => timingInfo;
  304. loader.on('segmenttimemapping', function() {
  305. segmenttimemappings++;
  306. });
  307. loader.playlist(playlist);
  308. loader.mimeType(this.mimeType);
  309. loader.load();
  310. this.clock.tick(1);
  311. assert.equal(segmenttimemappings, 0, 'no events before segment downloaded');
  312. // some time passes and a response is received
  313. this.requests[0].response = new Uint8Array(10).buffer;
  314. this.requests.shift().respond(200, null, '');
  315. assert.equal(segmenttimemappings, 0,
  316. 'did not trigger segmenttimemappings with unsuccessful probe');
  317. this.updateend();
  318. this.clock.tick(1);
  319. assert.equal(segmenttimemappings, 0, 'no events before segment downloaded');
  320. timingInfo.hasMapping = true;
  321. this.syncController.timelines[0] = { mapping: 0 };
  322. // some time passes and a response is received
  323. this.requests[0].response = new Uint8Array(10).buffer;
  324. this.requests.shift().respond(200, null, '');
  325. assert.equal(segmenttimemappings, 1,
  326. 'triggered segmenttimemappings with successful probe');
  327. });
  328. QUnit.test('adds cues with segment information to the segment-metadata track ' +
  329. 'as they are buffered',
  330. function(assert) {
  331. const track = loader.segmentMetadataTrack_;
  332. const attributes = {
  333. BANDWIDTH: 3500000,
  334. RESOLUTION: '1920x1080',
  335. CODECS: 'mp4a.40.5,avc1.42001e'
  336. };
  337. let playlist = playlistWithDuration(50, {attributes});
  338. let probeResponse;
  339. let expectedCue;
  340. loader.addSegmentMetadataCue_ = ogAddSegmentMetadataCue_;
  341. loader.syncController_.probeTsSegment_ = function(segmentInfo) {
  342. return probeResponse;
  343. };
  344. loader.playlist(playlist);
  345. loader.mimeType(this.mimeType);
  346. loader.load();
  347. this.clock.tick(1);
  348. assert.ok(!track.cues.length,
  349. 'segment-metadata track empty when no segments appended');
  350. // Start appending some segments
  351. probeResponse = { start: 0, end: 9.5 };
  352. this.requests[0].response = new Uint8Array(10).buffer;
  353. this.requests.shift().respond(200, null, '');
  354. this.updateend();
  355. this.clock.tick(1);
  356. expectedCue = {
  357. uri: '0.ts',
  358. timeline: 0,
  359. playlist: 'playlist.m3u8',
  360. start: 0,
  361. end: 9.5,
  362. bandwidth: 3500000,
  363. resolution: '1920x1080',
  364. codecs: 'mp4a.40.5,avc1.42001e',
  365. byteLength: 10
  366. };
  367. assert.equal(track.cues.length, 1, 'one cue added for segment');
  368. assert.deepEqual(track.cues[0].value, expectedCue,
  369. 'added correct segment info to cue');
  370. probeResponse = { start: 9.56, end: 19.2 };
  371. this.requests[0].response = new Uint8Array(10).buffer;
  372. this.requests.shift().respond(200, null, '');
  373. this.updateend();
  374. this.clock.tick(1);
  375. expectedCue = {
  376. uri: '1.ts',
  377. timeline: 0,
  378. playlist: 'playlist.m3u8',
  379. start: 9.56,
  380. end: 19.2,
  381. bandwidth: 3500000,
  382. resolution: '1920x1080',
  383. codecs: 'mp4a.40.5,avc1.42001e',
  384. byteLength: 10
  385. };
  386. assert.equal(track.cues.length, 2, 'one cue added for segment');
  387. assert.deepEqual(track.cues[1].value, expectedCue,
  388. 'added correct segment info to cue');
  389. probeResponse = { start: 19.24, end: 28.99 };
  390. this.requests[0].response = new Uint8Array(10).buffer;
  391. this.requests.shift().respond(200, null, '');
  392. this.updateend();
  393. this.clock.tick(1);
  394. expectedCue = {
  395. uri: '2.ts',
  396. timeline: 0,
  397. playlist: 'playlist.m3u8',
  398. start: 19.24,
  399. end: 28.99,
  400. bandwidth: 3500000,
  401. resolution: '1920x1080',
  402. codecs: 'mp4a.40.5,avc1.42001e',
  403. byteLength: 10
  404. };
  405. assert.equal(track.cues.length, 3, 'one cue added for segment');
  406. assert.deepEqual(track.cues[2].value, expectedCue,
  407. 'added correct segment info to cue');
  408. // append overlapping segment, emmulating segment-loader fetching behavior on
  409. // rendtion switch
  410. probeResponse = { start: 19.21, end: 28.98 };
  411. this.requests[0].response = new Uint8Array(10).buffer;
  412. this.requests.shift().respond(200, null, '');
  413. this.updateend();
  414. this.clock.tick(1);
  415. expectedCue = {
  416. uri: '3.ts',
  417. timeline: 0,
  418. playlist: 'playlist.m3u8',
  419. start: 19.21,
  420. end: 28.98,
  421. bandwidth: 3500000,
  422. resolution: '1920x1080',
  423. codecs: 'mp4a.40.5,avc1.42001e',
  424. byteLength: 10
  425. };
  426. assert.equal(track.cues.length, 3, 'overlapped cue removed, new one added');
  427. assert.deepEqual(track.cues[2].value, expectedCue,
  428. 'added correct segment info to cue');
  429. // does not add cue for invalid segment timing info
  430. probeResponse = { start: 30, end: void 0 };
  431. this.requests[0].response = new Uint8Array(10).buffer;
  432. this.requests.shift().respond(200, null, '');
  433. this.updateend();
  434. this.clock.tick(1);
  435. assert.equal(track.cues.length, 3, 'no cue added');
  436. // verify stats
  437. assert.equal(loader.mediaBytesTransferred, 50, '50 bytes');
  438. assert.equal(loader.mediaRequests, 5, '5 requests');
  439. });
  440. QUnit.test('fires ended at the end of a playlist', function(assert) {
  441. let endOfStreams = 0;
  442. let buffered = videojs.createTimeRanges();
  443. loader.buffered_ = () => buffered;
  444. loader.playlist(playlistWithDuration(10));
  445. loader.mimeType(this.mimeType);
  446. loader.load();
  447. this.clock.tick(1);
  448. loader.mediaSource_ = {
  449. readyState: 'open',
  450. sourceBuffers: this.mediaSource.sourceBuffers
  451. };
  452. loader.on('ended', () => endOfStreams++);
  453. this.requests[0].response = new Uint8Array(10).buffer;
  454. this.requests.shift().respond(200, null, '');
  455. buffered = videojs.createTimeRanges([[0, 10]]);
  456. this.updateend();
  457. this.clock.tick(1);
  458. assert.equal(endOfStreams, 1, 'triggered ended');
  459. // verify stats
  460. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  461. assert.equal(loader.mediaRequests, 1, '1 request');
  462. });
  463. QUnit.test('endOfStream happens even after a rendition switch', function(assert) {
  464. let endOfStreams = 0;
  465. let bandwidthupdates = 0;
  466. let buffered = videojs.createTimeRanges();
  467. loader.buffered_ = () => buffered;
  468. loader.playlist(playlistWithDuration(20));
  469. loader.mimeType(this.mimeType);
  470. loader.load();
  471. this.clock.tick(1);
  472. loader.mediaSource_ = {
  473. readyState: 'open',
  474. sourceBuffers: this.mediaSource.sourceBuffers
  475. };
  476. loader.on('ended', () => endOfStreams++);
  477. loader.on('bandwidthupdate', () => {
  478. bandwidthupdates++;
  479. // Simulate a rendition switch
  480. loader.resetEverything();
  481. });
  482. this.requests[0].response = new Uint8Array(10).buffer;
  483. this.requests.shift().respond(200, null, '');
  484. buffered = videojs.createTimeRanges([[0, 10]]);
  485. this.updateend();
  486. this.clock.tick(10);
  487. this.requests[0].response = new Uint8Array(10).buffer;
  488. this.requests.shift().respond(200, null, '');
  489. buffered = videojs.createTimeRanges([[0, 10]]);
  490. this.updateend();
  491. assert.equal(bandwidthupdates, 1, 'triggered bandwidthupdate');
  492. assert.equal(endOfStreams, 1, 'triggered ended');
  493. });
  494. QUnit.test('live playlists do not trigger ended', function(assert) {
  495. let endOfStreams = 0;
  496. let playlist;
  497. let buffered = videojs.createTimeRanges();
  498. loader.buffered_ = () => buffered;
  499. playlist = playlistWithDuration(10);
  500. playlist.endList = false;
  501. loader.playlist(playlist);
  502. loader.mimeType(this.mimeType);
  503. loader.load();
  504. this.clock.tick(1);
  505. loader.mediaSource_ = {
  506. readyState: 'open',
  507. sourceBuffers: this.mediaSource.sourceBuffers
  508. };
  509. loader.on('ended', () => endOfStreams++);
  510. this.requests[0].response = new Uint8Array(10).buffer;
  511. this.requests.shift().respond(200, null, '');
  512. buffered = videojs.createTimeRanges([[0, 10]]);
  513. this.updateend();
  514. this.clock.tick(1);
  515. assert.equal(endOfStreams, 0, 'did not trigger ended');
  516. // verify stats
  517. assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
  518. assert.equal(loader.mediaRequests, 1, '1 request');
  519. });
  520. QUnit.test('saves segment info to new segment after playlist refresh',
  521. function(assert) {
  522. let playlist = playlistWithDuration(40);
  523. let buffered = videojs.createTimeRanges();
  524. loader.buffered_ = () => buffered;
  525. playlist.endList = false;
  526. loader.playlist(playlist);
  527. loader.mimeType(this.mimeType);
  528. loader.load();
  529. this.clock.tick(1);
  530. assert.equal(loader.state, 'WAITING', 'in waiting state');
  531. assert.equal(loader.pendingSegment_.uri, '0.ts', 'first segment pending');
  532. assert.equal(loader.pendingSegment_.segment.uri,
  533. '0.ts',
  534. 'correct segment reference');
  535. // wrap up the first request to set mediaIndex and start normal live streaming
  536. this.requests[0].response = new Uint8Array(10).buffer;
  537. this.requests.shift().respond(200, null, '');
  538. buffered = videojs.createTimeRanges([[0, 10]]);
  539. this.updateend();
  540. this.clock.tick(1);
  541. assert.equal(loader.state, 'WAITING', 'in waiting state');
  542. assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment pending');
  543. assert.equal(loader.pendingSegment_.segment.uri,
  544. '1.ts',
  545. 'correct segment reference');
  546. // playlist updated during waiting
  547. let playlistUpdated = playlistWithDuration(40);
  548. playlistUpdated.segments.shift();
  549. playlistUpdated.mediaSequence++;
  550. loader.playlist(playlistUpdated);
  551. assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment still pending');
  552. assert.equal(loader.pendingSegment_.segment.uri,
  553. '1.ts',
  554. 'correct segment reference');
  555. // mock probeSegmentInfo as the response bytes aren't parsable (and won't provide
  556. // time info)
  557. loader.syncController_.probeSegmentInfo = (segmentInfo) => {
  558. segmentInfo.segment.start = 10;
  559. segmentInfo.segment.end = 20;
  560. };
  561. this.requests[0].response = new Uint8Array(10).buffer;
  562. this.requests.shift().respond(200, null, '');
  563. assert.equal(playlistUpdated.segments[0].start,
  564. 10,
  565. 'set start on segment of new playlist');
  566. assert.equal(playlistUpdated.segments[0].end,
  567. 20,
  568. 'set end on segment of new playlist');
  569. assert.ok(!playlist.segments[1].start,
  570. 'did not set start on segment of old playlist');
  571. assert.ok(!playlist.segments[1].end, 'did not set end on segment of old playlist');
  572. });
  573. QUnit.test(
  574. 'saves segment info to old segment after playlist refresh if segment fell off',
  575. function(assert) {
  576. let playlist = playlistWithDuration(40);
  577. let buffered = videojs.createTimeRanges();
  578. loader.buffered_ = () => buffered;
  579. playlist.endList = false;
  580. loader.playlist(playlist);
  581. loader.mimeType(this.mimeType);
  582. loader.load();
  583. this.clock.tick(1);
  584. assert.equal(loader.state, 'WAITING', 'in waiting state');
  585. assert.equal(loader.pendingSegment_.uri, '0.ts', 'first segment pending');
  586. assert.equal(loader.pendingSegment_.segment.uri,
  587. '0.ts',
  588. 'correct segment reference');
  589. // wrap up the first request to set mediaIndex and start normal live streaming
  590. this.requests[0].response = new Uint8Array(10).buffer;
  591. this.requests.shift().respond(200, null, '');
  592. buffered = videojs.createTimeRanges([[0, 10]]);
  593. this.updateend();
  594. this.clock.tick(1);
  595. assert.equal(loader.state, 'WAITING', 'in waiting state');
  596. assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment pending');
  597. assert.equal(loader.pendingSegment_.segment.uri,
  598. '1.ts',
  599. 'correct segment reference');
  600. // playlist updated during waiting
  601. let playlistUpdated = playlistWithDuration(40);
  602. playlistUpdated.segments.shift();
  603. playlistUpdated.segments.shift();
  604. playlistUpdated.mediaSequence += 2;
  605. loader.playlist(playlistUpdated);
  606. assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment still pending');
  607. assert.equal(loader.pendingSegment_.segment.uri,
  608. '1.ts',
  609. 'correct segment reference');
  610. // mock probeSegmentInfo as the response bytes aren't parsable (and won't provide
  611. // time info)
  612. loader.syncController_.probeSegmentInfo = (segmentInfo) => {
  613. segmentInfo.segment.start = 10;
  614. segmentInfo.segment.end = 20;
  615. };
  616. this.requests[0].response = new Uint8Array(10).buffer;
  617. this.requests.shift().respond(200, null, '');
  618. assert.equal(playlist.segments[1].start,
  619. 10,
  620. 'set start on segment of old playlist');
  621. assert.equal(playlist.segments[1].end,
  622. 20,
  623. 'set end on segment of old playlist');
  624. assert.ok(!playlistUpdated.segments[0].start,
  625. 'no start info for first segment of new playlist');
  626. assert.ok(!playlistUpdated.segments[0].end,
  627. 'no end info for first segment of new playlist');
  628. });
  629. QUnit.test('errors when trying to switch from audio and video to audio only',
  630. function(assert) {
  631. const playlist = playlistWithDuration(40);
  632. const errors = [];
  633. loader.on('error', () => errors.push(loader.error()));
  634. loader.playlist(playlist);
  635. loader.mimeType(this.mimeType);
  636. loader.load();
  637. this.clock.tick(1);
  638. loader.syncController_.probeSegmentInfo = () => {
  639. return {
  640. start: 0,
  641. end: 10,
  642. containsAudio: true,
  643. containsVideo: true
  644. };
  645. };
  646. this.requests[0].response = new Uint8Array(10).buffer;
  647. this.requests.shift().respond(200, null, '');
  648. loader.buffered_ = () => videojs.createTimeRanges([[0, 10]]);
  649. this.updateend();
  650. this.clock.tick(1);
  651. assert.equal(errors.length, 0, 'no errors');
  652. loader.syncController_.probeSegmentInfo = () => {
  653. return {
  654. start: 10,
  655. end: 20,
  656. containsAudio: true,
  657. containsVideo: false
  658. };
  659. };
  660. this.requests[0].response = new Uint8Array(10).buffer;
  661. this.requests.shift().respond(200, null, '');
  662. assert.equal(errors.length, 1, 'one error');
  663. assert.equal(errors[0].message,
  664. 'Only audio found in segment when we expected video.' +
  665. ' We can\'t switch to audio only from a stream that had video.' +
  666. ' To get rid of this message, please add codec information to the' +
  667. ' manifest.',
  668. 'correct error message');
  669. });
  670. QUnit.test('errors when trying to switch from audio only to audio and video',
  671. function(assert) {
  672. const playlist = playlistWithDuration(40);
  673. const errors = [];
  674. loader.on('error', () => errors.push(loader.error()));
  675. loader.playlist(playlist);
  676. loader.mimeType(this.mimeType);
  677. loader.load();
  678. this.clock.tick(1);
  679. loader.syncController_.probeSegmentInfo = () => {
  680. return {
  681. start: 0,
  682. end: 10,
  683. containsAudio: true,
  684. containsVideo: false
  685. };
  686. };
  687. this.requests[0].response = new Uint8Array(10).buffer;
  688. this.requests.shift().respond(200, null, '');
  689. loader.buffered_ = () => videojs.createTimeRanges([[0, 10]]);
  690. this.updateend();
  691. this.clock.tick(1);
  692. assert.equal(errors.length, 0, 'no errors');
  693. loader.syncController_.probeSegmentInfo = () => {
  694. return {
  695. start: 10,
  696. end: 20,
  697. containsAudio: true,
  698. containsVideo: true
  699. };
  700. };
  701. this.requests[0].response = new Uint8Array(10).buffer;
  702. this.requests.shift().respond(200, null, '');
  703. assert.equal(errors.length, 1, 'one error');
  704. assert.equal(errors[0].message,
  705. 'Video found in segment when we expected only audio.' +
  706. ' We can\'t switch to a stream with video from an audio only stream.' +
  707. ' To get rid of this message, please add codec information to the' +
  708. ' manifest.',
  709. 'correct error message');
  710. });
  711. QUnit.test('no error when not switching from audio and video', function(assert) {
  712. const playlist = playlistWithDuration(40);
  713. const errors = [];
  714. loader.on('error', () => errors.push(loader.error()));
  715. loader.playlist(playlist);
  716. loader.mimeType(this.mimeType);
  717. loader.load();
  718. this.clock.tick(1);
  719. loader.syncController_.probeSegmentInfo = () => {
  720. return {
  721. start: 0,
  722. end: 10,
  723. containsAudio: true,
  724. containsVideo: true
  725. };
  726. };
  727. this.requests[0].response = new Uint8Array(10).buffer;
  728. this.requests.shift().respond(200, null, '');
  729. loader.buffered_ = () => videojs.createTimeRanges([[0, 10]]);
  730. this.updateend();
  731. this.clock.tick(1);
  732. assert.equal(errors.length, 0, 'no errors');
  733. loader.syncController_.probeSegmentInfo = () => {
  734. return {
  735. start: 10,
  736. end: 20,
  737. containsAudio: true,
  738. containsVideo: true
  739. };
  740. };
  741. this.requests[0].response = new Uint8Array(10).buffer;
  742. this.requests.shift().respond(200, null, '');
  743. assert.equal(errors.length, 0, 'no errors');
  744. });
  745. });
  746. });