You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2557 lines
78 KiB

  1. (function(window, videojs, undefined) {
  2. 'use strict';
  3. /*
  4. ======== A Handy Little QUnit Reference ========
  5. http://api.qunitjs.com/
  6. Test methods:
  7. module(name, {[setup][ ,teardown]})
  8. test(name, callback)
  9. expect(numberOfAssertions)
  10. stop(increment)
  11. start(decrement)
  12. Test assertions:
  13. ok(value, [message])
  14. equal(actual, expected, [message])
  15. notEqual(actual, expected, [message])
  16. deepEqual(actual, expected, [message])
  17. notDeepEqual(actual, expected, [message])
  18. strictEqual(actual, expected, [message])
  19. notStrictEqual(actual, expected, [message])
  20. throws(block, [expected], [message])
  21. */
  22. var
  23. Flash = videojs.getComponent('Flash'),
  24. oldFlash,
  25. player,
  26. clock,
  27. oldMediaSource,
  28. oldCreateUrl,
  29. oldSegmentParser,
  30. oldSourceBuffer,
  31. oldFlashSupported,
  32. oldNativeHlsSupport,
  33. oldDecrypt,
  34. oldGlobalOptions,
  35. requests,
  36. xhr,
  37. nextId = 0,
  38. // patch over some methods of the provided tech so it can be tested
  39. // synchronously with sinon's fake timers
  40. mockTech = function(tech) {
  41. if (tech.isMocked_) {
  42. // make this function idempotent because HTML and Flash based
  43. // playback have very different lifecycles. For HTML, the tech
  44. // is available on player creation. For Flash, the tech isn't
  45. // ready until the source has been loaded and one tick has
  46. // expired.
  47. return;
  48. }
  49. tech.isMocked_ = true;
  50. tech.paused_ = !tech.autoplay();
  51. tech.paused = function() {
  52. return tech.paused_;
  53. };
  54. if (!tech.currentTime_) {
  55. tech.currentTime_ = tech.currentTime;
  56. }
  57. tech.currentTime = function() {
  58. return tech.time_ === undefined ? tech.currentTime_() : tech.time_;
  59. };
  60. tech.setSrc = function(src) {
  61. tech.src_ = src;
  62. };
  63. tech.src = function(src) {
  64. if (src !== undefined) {
  65. return tech.setSrc(src);
  66. }
  67. return tech.src_ === undefined ? tech.src : tech.src_;
  68. };
  69. tech.currentSrc_ = tech.currentSrc;
  70. tech.currentSrc = function() {
  71. return tech.src_ === undefined ? tech.currentSrc_() : tech.src_;
  72. };
  73. tech.play_ = tech.play;
  74. tech.play = function() {
  75. tech.play_();
  76. tech.paused_ = false;
  77. tech.trigger('play');
  78. };
  79. tech.pause_ = tech.pause_;
  80. tech.pause = function() {
  81. tech.pause_();
  82. tech.paused_ = true;
  83. tech.trigger('pause');
  84. };
  85. tech.setCurrentTime = function(time) {
  86. tech.time_ = time;
  87. setTimeout(function() {
  88. tech.trigger('seeking');
  89. setTimeout(function() {
  90. tech.trigger('seeked');
  91. }, 1);
  92. }, 1);
  93. };
  94. },
  95. createPlayer = function(options) {
  96. var video, player;
  97. video = document.createElement('video');
  98. video.className = 'video-js';
  99. document.querySelector('#qunit-fixture').appendChild(video);
  100. player = videojs(video, options || {
  101. flash: {
  102. swf: ''
  103. }
  104. });
  105. player.buffered = function() {
  106. return videojs.createTimeRange(0, 0);
  107. };
  108. mockTech(player.tech_);
  109. return player;
  110. },
  111. openMediaSource = function(player) {
  112. // ensure the Flash tech is ready
  113. player.tech_.triggerReady();
  114. clock.tick(1);
  115. mockTech(player.tech_);
  116. // simulate the sourceopen event
  117. player.tech_.hls.mediaSource.readyState = 'open';
  118. player.tech_.hls.mediaSource.dispatchEvent({
  119. type: 'sourceopen',
  120. swfId: player.tech_.el().id
  121. });
  122. // endOfStream triggers an exception if flash isn't available
  123. player.tech_.hls.mediaSource.endOfStream = function(error) {
  124. this.error_ = error;
  125. };
  126. },
  127. standardXHRResponse = function(request) {
  128. if (!request.url) {
  129. return;
  130. }
  131. var contentType = "application/json",
  132. // contents off the global object
  133. manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
  134. if (manifestName) {
  135. manifestName = manifestName[1];
  136. } else {
  137. manifestName = request.url;
  138. }
  139. if (/\.m3u8?/.test(request.url)) {
  140. contentType = 'application/vnd.apple.mpegurl';
  141. } else if (/\.ts/.test(request.url)) {
  142. contentType = 'video/MP2T';
  143. }
  144. request.response = new Uint8Array(16).buffer;
  145. request.respond(200,
  146. { 'Content-Type': contentType },
  147. window.manifests[manifestName]);
  148. },
  149. // a no-op MediaSource implementation to allow synchronous testing
  150. MockMediaSource = videojs.extend(videojs.EventTarget, {
  151. constructor: function() {},
  152. addSourceBuffer: function() {
  153. return new (videojs.extend(videojs.EventTarget, {
  154. constructor: function() {},
  155. abort: function() {},
  156. buffered: videojs.createTimeRange(),
  157. appendBuffer: function() {},
  158. remove: function() {}
  159. }))();
  160. },
  161. endOfStream: function() {}
  162. }),
  163. // do a shallow copy of the properties of source onto the target object
  164. merge = function(target, source) {
  165. var name;
  166. for (name in source) {
  167. target[name] = source[name];
  168. }
  169. },
  170. // return an absolute version of a page-relative URL
  171. absoluteUrl = function(relativeUrl) {
  172. return window.location.protocol + '//' +
  173. window.location.host +
  174. (window.location.pathname
  175. .split('/')
  176. .slice(0, -1)
  177. .concat(relativeUrl)
  178. .join('/'));
  179. };
  180. MockMediaSource.open = function() {};
  181. module('HLS', {
  182. beforeEach: function() {
  183. oldMediaSource = videojs.MediaSource;
  184. videojs.MediaSource = MockMediaSource;
  185. oldCreateUrl = videojs.URL.createObjectURL;
  186. videojs.URL.createObjectURL = function() {
  187. return 'blob:mock-vjs-object-url';
  188. };
  189. // mock out Flash features for phantomjs
  190. oldFlash = videojs.mergeOptions({}, Flash);
  191. Flash.embed = function(swf, flashVars) {
  192. var el = document.createElement('div');
  193. el.id = 'vjs_mock_flash_' + nextId++;
  194. el.className = 'vjs-tech vjs-mock-flash';
  195. el.duration = Infinity;
  196. el.vjs_load = function() {};
  197. el.vjs_getProperty = function(attr) {
  198. if (attr === 'buffered') {
  199. return [[0,0]];
  200. }
  201. return el[attr];
  202. };
  203. el.vjs_setProperty = function(attr, value) {
  204. el[attr] = value;
  205. };
  206. el.vjs_src = function() {};
  207. el.vjs_play = function() {};
  208. el.vjs_discontinuity = function() {};
  209. if (flashVars.autoplay) {
  210. el.autoplay = true;
  211. }
  212. if (flashVars.preload) {
  213. el.preload = flashVars.preload;
  214. }
  215. el.currentTime = 0;
  216. return el;
  217. };
  218. oldFlashSupported = Flash.isSupported;
  219. Flash.isSupported = function() {
  220. return true;
  221. };
  222. oldSourceBuffer = window.videojs.SourceBuffer;
  223. window.videojs.SourceBuffer = function() {
  224. this.appendBuffer = function() {};
  225. this.abort = function() {};
  226. };
  227. // store functionality that some tests need to mock
  228. oldSegmentParser = videojs.Hls.SegmentParser;
  229. oldGlobalOptions = videojs.mergeOptions(videojs.options);
  230. // force the HLS tech to run
  231. oldNativeHlsSupport = videojs.Hls.supportsNativeHls;
  232. videojs.Hls.supportsNativeHls = false;
  233. oldDecrypt = videojs.Hls.Decrypter;
  234. videojs.Hls.Decrypter = function() {};
  235. // fake XHRs
  236. xhr = sinon.useFakeXMLHttpRequest();
  237. videojs.xhr.XMLHttpRequest = xhr;
  238. requests = [];
  239. xhr.onCreate = function(xhr) {
  240. requests.push(xhr);
  241. };
  242. // fake timers
  243. clock = sinon.useFakeTimers();
  244. // create the test player
  245. player = createPlayer();
  246. },
  247. afterEach: function() {
  248. videojs.MediaSource = oldMediaSource;
  249. videojs.URL.createObjectURL = oldCreateUrl;
  250. merge(videojs.options, oldGlobalOptions);
  251. Flash.isSupported = oldFlashSupported;
  252. merge(Flash, oldFlash);
  253. videojs.Hls.SegmentParser = oldSegmentParser;
  254. videojs.Hls.supportsNativeHls = oldNativeHlsSupport;
  255. videojs.Hls.Decrypter = oldDecrypt;
  256. videojs.SourceBuffer = oldSourceBuffer;
  257. player.dispose();
  258. xhr.restore();
  259. videojs.xhr.XMLHttpRequest = window.XMLHttpRequest;
  260. clock.restore();
  261. }
  262. });
  263. test('starts playing if autoplay is specified', function() {
  264. var plays = 0;
  265. player.autoplay(true);
  266. player.src({
  267. src: 'manifest/playlist.m3u8',
  268. type: 'application/vnd.apple.mpegurl'
  269. });
  270. // REMOVEME workaround https://github.com/videojs/video.js/issues/2326
  271. player.tech_.triggerReady();
  272. clock.tick(1);
  273. // make sure play() is called *after* the media source opens
  274. player.tech_.hls.play = function() {
  275. plays++;
  276. };
  277. openMediaSource(player);
  278. standardXHRResponse(requests[0]);
  279. strictEqual(1, plays, 'play was called');
  280. });
  281. test('autoplay seeks to the live point after playlist load', function() {
  282. var currentTime = 0;
  283. player.autoplay(true);
  284. player.on('seeking', function() {
  285. currentTime = player.currentTime();
  286. });
  287. player.src({
  288. src: 'liveStart30sBefore.m3u8',
  289. type: 'application/vnd.apple.mpegurl'
  290. });
  291. openMediaSource(player);
  292. standardXHRResponse(requests.shift());
  293. clock.tick(1);
  294. notEqual(currentTime, 0, 'seeked on autoplay');
  295. });
  296. test('autoplay seeks to the live point after media source open', function() {
  297. var currentTime = 0;
  298. player.autoplay(true);
  299. player.on('seeking', function() {
  300. currentTime = player.currentTime();
  301. });
  302. player.src({
  303. src: 'liveStart30sBefore.m3u8',
  304. type: 'application/vnd.apple.mpegurl'
  305. });
  306. player.tech_.triggerReady();
  307. clock.tick(1);
  308. standardXHRResponse(requests.shift());
  309. openMediaSource(player);
  310. clock.tick(1);
  311. notEqual(currentTime, 0, 'seeked on autoplay');
  312. });
  313. test('duration is set when the source opens after the playlist is loaded', function() {
  314. player.src({
  315. src: 'media.m3u8',
  316. type: 'application/vnd.apple.mpegurl'
  317. });
  318. player.tech_.triggerReady();
  319. clock.tick(1);
  320. standardXHRResponse(requests.shift());
  321. openMediaSource(player);
  322. equal(player.tech_.hls.mediaSource.duration , 40, 'set the duration');
  323. });
  324. test('codecs are passed to the source buffer', function() {
  325. var codecs = [];
  326. player.src({
  327. src: 'custom-codecs.m3u8',
  328. type: 'application/vnd.apple.mpegurl'
  329. });
  330. openMediaSource(player);
  331. player.tech_.hls.mediaSource.addSourceBuffer = function(codec) {
  332. codecs.push(codec);
  333. };
  334. requests.shift().respond(200, null,
  335. '#EXTM3U\n' +
  336. '#EXT-X-STREAM-INF:CODECS="video, audio"\n' +
  337. 'media.m3u8\n');
  338. standardXHRResponse(requests.shift());
  339. equal(codecs.length, 1, 'created a source buffer');
  340. equal(codecs[0], 'video/mp2t; codecs="video, audio"', 'specified the codecs');
  341. });
  342. test('including HLS as a tech does not error', function() {
  343. var player = createPlayer({
  344. techOrder: ['hls', 'html5']
  345. });
  346. ok(player, 'created the player');
  347. });
  348. test('creates a PlaylistLoader on init', function() {
  349. player.src({
  350. src: 'manifest/playlist.m3u8',
  351. type: 'application/vnd.apple.mpegurl'
  352. });
  353. openMediaSource(player);
  354. player.src({
  355. src:'manifest/playlist.m3u8',
  356. type: 'application/vnd.apple.mpegurl'
  357. });
  358. openMediaSource(player);
  359. equal(requests[0].aborted, true, 'aborted previous src');
  360. standardXHRResponse(requests[1]);
  361. ok(player.tech_.hls.playlists.master, 'set the master playlist');
  362. ok(player.tech_.hls.playlists.media(), 'set the media playlist');
  363. ok(player.tech_.hls.playlists.media().segments, 'the segment entries are parsed');
  364. strictEqual(player.tech_.hls.playlists.master.playlists[0],
  365. player.tech_.hls.playlists.media(),
  366. 'the playlist is selected');
  367. });
  368. test('re-initializes the playlist loader when switching sources', function() {
  369. // source is set
  370. player.src({
  371. src:'manifest/media.m3u8',
  372. type: 'application/vnd.apple.mpegurl'
  373. });
  374. openMediaSource(player);
  375. // loader gets media playlist
  376. standardXHRResponse(requests.shift());
  377. // request a segment
  378. standardXHRResponse(requests.shift());
  379. // change the source
  380. player.src({
  381. src:'manifest/master.m3u8',
  382. type: 'application/vnd.apple.mpegurl'
  383. });
  384. // maybe not needed if https://github.com/videojs/video.js/issues/2326 gets fixed
  385. clock.tick(1);
  386. ok(!player.tech_.hls.playlists.media(), 'no media playlist');
  387. equal(player.tech_.hls.playlists.state,
  388. 'HAVE_NOTHING',
  389. 'reset the playlist loader state');
  390. equal(requests.length, 1, 'requested the new src');
  391. // buffer check
  392. player.tech_.hls.checkBuffer_();
  393. equal(requests.length, 1, 'did not request a stale segment');
  394. // sourceopen
  395. openMediaSource(player);
  396. equal(requests.length, 1, 'made one request');
  397. ok(requests[0].url.indexOf('master.m3u8') >= 0, 'requested only the new playlist');
  398. });
  399. test('sets the duration if one is available on the playlist', function() {
  400. var events = 0;
  401. player.src({
  402. src: 'manifest/media.m3u8',
  403. type: 'application/vnd.apple.mpegurl'
  404. });
  405. openMediaSource(player);
  406. player.tech_.on('durationchange', function() {
  407. events++;
  408. });
  409. standardXHRResponse(requests[0]);
  410. equal(player.tech_.hls.mediaSource.duration, 40, 'set the duration');
  411. equal(events, 1, 'durationchange is fired');
  412. });
  413. test('estimates individual segment durations if needed', function() {
  414. var changes = 0;
  415. player.src({
  416. src: 'http://example.com/manifest/missingExtinf.m3u8',
  417. type: 'application/vnd.apple.mpegurl'
  418. });
  419. openMediaSource(player);
  420. player.tech_.hls.mediaSource.duration = NaN;
  421. player.tech_.on('durationchange', function() {
  422. changes++;
  423. });
  424. standardXHRResponse(requests[0]);
  425. strictEqual(player.tech_.hls.mediaSource.duration,
  426. player.tech_.hls.playlists.media().segments.length * 10,
  427. 'duration is updated');
  428. strictEqual(changes, 1, 'one durationchange fired');
  429. });
  430. test('translates seekable by the starting time for live playlists', function() {
  431. var seekable;
  432. player.src({
  433. src: 'media.m3u8',
  434. type: 'application/vnd.apple.mpegurl'
  435. });
  436. openMediaSource(player);
  437. requests.shift().respond(200, null,
  438. '#EXTM3U\n' +
  439. '#EXT-X-MEDIA-SEQUENCE:15\n' +
  440. '#EXTINF:10,\n' +
  441. '0.ts\n' +
  442. '#EXTINF:10,\n' +
  443. '1.ts\n' +
  444. '#EXTINF:10,\n' +
  445. '2.ts\n' +
  446. '#EXTINF:10,\n' +
  447. '3.ts\n');
  448. seekable = player.seekable();
  449. equal(seekable.length, 1, 'one seekable range');
  450. equal(seekable.start(0), 0, 'the earliest possible position is at zero');
  451. equal(seekable.end(0), 10, 'end is relative to the start');
  452. });
  453. test('starts downloading a segment on loadedmetadata', function() {
  454. player.src({
  455. src: 'manifest/media.m3u8',
  456. type: 'application/vnd.apple.mpegurl'
  457. });
  458. player.buffered = function() {
  459. return videojs.createTimeRange(0, 0);
  460. };
  461. openMediaSource(player);
  462. standardXHRResponse(requests[0]);
  463. standardXHRResponse(requests[1]);
  464. strictEqual(requests[1].url,
  465. absoluteUrl('manifest/media-00001.ts'),
  466. 'the first segment is requested');
  467. });
  468. test('finds the correct buffered region based on currentTime', function() {
  469. player.src({
  470. src: 'manifest/media.m3u8',
  471. type: 'application/vnd.apple.mpegurl'
  472. });
  473. player.tech_.buffered = function() {
  474. return videojs.createTimeRanges([[0, 5], [6, 12]]);
  475. };
  476. openMediaSource(player);
  477. standardXHRResponse(requests[0]);
  478. standardXHRResponse(requests[1]);
  479. player.currentTime(3);
  480. clock.tick(1);
  481. equal(player.tech_.hls.findCurrentBuffered_().end(0),
  482. 5, 'inside the first buffered region');
  483. player.currentTime(6);
  484. clock.tick(1);
  485. equal(player.tech_.hls.findCurrentBuffered_().end(0),
  486. 12, 'inside the second buffered region');
  487. });
  488. test('recognizes absolute URIs and requests them unmodified', function() {
  489. player.src({
  490. src: 'manifest/absoluteUris.m3u8',
  491. type: 'application/vnd.apple.mpegurl'
  492. });
  493. openMediaSource(player);
  494. standardXHRResponse(requests[0]);
  495. standardXHRResponse(requests[1]);
  496. strictEqual(requests[1].url,
  497. 'http://example.com/00001.ts',
  498. 'the first segment is requested');
  499. });
  500. test('recognizes domain-relative URLs', function() {
  501. player.src({
  502. src: 'manifest/domainUris.m3u8',
  503. type: 'application/vnd.apple.mpegurl'
  504. });
  505. openMediaSource(player);
  506. standardXHRResponse(requests[0]);
  507. standardXHRResponse(requests[1]);
  508. strictEqual(requests[1].url,
  509. window.location.protocol + '//' + window.location.host +
  510. '/00001.ts',
  511. 'the first segment is requested');
  512. });
  513. test('re-initializes the handler for each source', function() {
  514. var firstPlaylists, secondPlaylists, firstMSE, secondMSE, aborts;
  515. aborts = 0;
  516. player.src({
  517. src: 'manifest/master.m3u8',
  518. type: 'application/vnd.apple.mpegurl'
  519. });
  520. openMediaSource(player);
  521. firstPlaylists = player.tech_.hls.playlists;
  522. firstMSE = player.tech_.hls.mediaSource;
  523. standardXHRResponse(requests.shift());
  524. standardXHRResponse(requests.shift());
  525. player.tech_.hls.sourceBuffer.abort = function() {
  526. aborts++;
  527. };
  528. player.src({
  529. src: 'manifest/master.m3u8',
  530. type: 'application/vnd.apple.mpegurl'
  531. });
  532. openMediaSource(player);
  533. secondPlaylists = player.tech_.hls.playlists;
  534. secondMSE = player.tech_.hls.mediaSource;
  535. equal(1, aborts, 'aborted the old source buffer');
  536. ok(requests[0].aborted, 'aborted the old segment request');
  537. notStrictEqual(firstPlaylists, secondPlaylists, 'the playlist object is not reused');
  538. notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused');
  539. });
  540. test('triggers an error when a master playlist request errors', function() {
  541. player.src({
  542. src: 'manifest/master.m3u8',
  543. type: 'application/vnd.apple.mpegurl'
  544. });
  545. openMediaSource(player);
  546. requests.pop().respond(500);
  547. equal(player.tech_.hls.mediaSource.error_, 'network', 'a network error is triggered');
  548. });
  549. test('downloads media playlists after loading the master', function() {
  550. player.src({
  551. src: 'manifest/master.m3u8',
  552. type: 'application/vnd.apple.mpegurl'
  553. });
  554. openMediaSource(player);
  555. player.tech_.hls.bandwidth = 20e10;
  556. standardXHRResponse(requests[0]);
  557. standardXHRResponse(requests[1]);
  558. standardXHRResponse(requests[2]);
  559. strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
  560. strictEqual(requests[1].url,
  561. absoluteUrl('manifest/media3.m3u8'),
  562. 'media playlist requested');
  563. strictEqual(requests[2].url,
  564. absoluteUrl('manifest/media3-00001.ts'),
  565. 'first segment requested');
  566. });
  567. test('upshifts if the initial bandwidth hint is high', function() {
  568. player.src({
  569. src: 'manifest/master.m3u8',
  570. type: 'application/vnd.apple.mpegurl'
  571. });
  572. openMediaSource(player);
  573. player.tech_.hls.bandwidth = 10e20;
  574. standardXHRResponse(requests[0]);
  575. standardXHRResponse(requests[1]);
  576. standardXHRResponse(requests[2]);
  577. strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
  578. strictEqual(requests[1].url,
  579. absoluteUrl('manifest/media3.m3u8'),
  580. 'media playlist requested');
  581. strictEqual(requests[2].url,
  582. absoluteUrl('manifest/media3-00001.ts'),
  583. 'first segment requested');
  584. });
  585. test('downshifts if the initial bandwidth hint is low', function() {
  586. player.src({
  587. src: 'manifest/master.m3u8',
  588. type: 'application/vnd.apple.mpegurl'
  589. });
  590. openMediaSource(player);
  591. player.tech_.hls.bandwidth = 100;
  592. standardXHRResponse(requests[0]);
  593. standardXHRResponse(requests[1]);
  594. standardXHRResponse(requests[2]);
  595. strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
  596. strictEqual(requests[1].url,
  597. absoluteUrl('manifest/media1.m3u8'),
  598. 'media playlist requested');
  599. strictEqual(requests[2].url,
  600. absoluteUrl('manifest/media1-00001.ts'),
  601. 'first segment requested');
  602. });
  603. test('starts checking the buffer on init', function() {
  604. var player, fills = 0, drains = 0;
  605. player = createPlayer();
  606. player.src({
  607. src: 'manifest/master.m3u8',
  608. type: 'application/vnd.apple.mpegurl'
  609. });
  610. openMediaSource(player);
  611. // wait long enough for the buffer check interval to expire and
  612. // trigger fill/drainBuffer
  613. player.tech_.hls.fillBuffer = function() {
  614. fills++;
  615. };
  616. player.tech_.hls.drainBuffer = function() {
  617. drains++;
  618. };
  619. clock.tick(500);
  620. equal(fills, 1, 'called fillBuffer');
  621. equal(drains, 1, 'called drainBuffer');
  622. player.dispose();
  623. clock.tick(100 * 1000);
  624. equal(fills, 1, 'did not call fillBuffer again');
  625. equal(drains, 1, 'did not call drainBuffer again');
  626. });
  627. test('buffer checks are noops until a media playlist is ready', function() {
  628. player.src({
  629. src: 'manifest/media.m3u8',
  630. type: 'application/vnd.apple.mpegurl'
  631. });
  632. openMediaSource(player);
  633. player.tech_.hls.checkBuffer_();
  634. strictEqual(1, requests.length, 'one request was made');
  635. strictEqual(requests[0].url, 'manifest/media.m3u8', 'media playlist requested');
  636. });
  637. test('buffer checks are noops when only the master is ready', function() {
  638. player.src({
  639. src: 'manifest/master.m3u8',
  640. type: 'application/vnd.apple.mpegurl'
  641. });
  642. openMediaSource(player);
  643. standardXHRResponse(requests.shift());
  644. standardXHRResponse(requests.shift());
  645. // ignore any outstanding segment requests
  646. requests.length = 0;
  647. // load in a new playlist which will cause playlists.media() to be
  648. // undefined while it is being fetched
  649. player.src({
  650. src: 'manifest/master.m3u8',
  651. type: 'application/vnd.apple.mpegurl'
  652. });
  653. openMediaSource(player);
  654. // respond with the master playlist but don't send the media playlist yet
  655. standardXHRResponse(requests.shift());
  656. // trigger fillBuffer()
  657. player.tech_.hls.checkBuffer_();
  658. strictEqual(1, requests.length, 'one request was made');
  659. strictEqual(requests[0].url,
  660. absoluteUrl('manifest/media1.m3u8'),
  661. 'media playlist requested');
  662. });
  663. test('calculates the bandwidth after downloading a segment', function() {
  664. player.src({
  665. src: 'manifest/media.m3u8',
  666. type: 'application/vnd.apple.mpegurl'
  667. });
  668. openMediaSource(player);
  669. standardXHRResponse(requests[0]);
  670. // set the request time to be a bit earlier so our bandwidth calculations are not NaN
  671. requests[1].requestTime = (new Date())-100;
  672. standardXHRResponse(requests[1]);
  673. ok(player.tech_.hls.bandwidth, 'bandwidth is calculated');
  674. ok(player.tech_.hls.bandwidth > 0,
  675. 'bandwidth is positive: ' + player.tech_.hls.bandwidth);
  676. ok(player.tech_.hls.segmentXhrTime >= 0,
  677. 'saves segment request time: ' + player.tech_.hls.segmentXhrTime + 's');
  678. });
  679. test('fires a progress event after downloading a segment', function() {
  680. var progressCount = 0;
  681. player.src({
  682. src: 'manifest/media.m3u8',
  683. type: 'application/vnd.apple.mpegurl'
  684. });
  685. openMediaSource(player);
  686. standardXHRResponse(requests.shift());
  687. player.on('progress', function() {
  688. progressCount++;
  689. });
  690. standardXHRResponse(requests.shift());
  691. equal(progressCount, 1, 'fired a progress event');
  692. });
  693. test('selects a playlist after segment downloads', function() {
  694. var calls = 0;
  695. player.src({
  696. src: 'manifest/master.m3u8',
  697. type: 'application/vnd.apple.mpegurl'
  698. });
  699. openMediaSource(player);
  700. player.tech_.hls.selectPlaylist = function() {
  701. calls++;
  702. return player.tech_.hls.playlists.master.playlists[0];
  703. };
  704. standardXHRResponse(requests[0]); // master
  705. standardXHRResponse(requests[1]); // media
  706. standardXHRResponse(requests[2]); // segment
  707. strictEqual(calls, 2, 'selects after the initial segment');
  708. player.currentTime = function() {
  709. return 1;
  710. };
  711. player.buffered = function() {
  712. return videojs.createTimeRange(0, 2);
  713. };
  714. player.tech_.hls.sourceBuffer.trigger('updateend');
  715. player.tech_.hls.checkBuffer_();
  716. standardXHRResponse(requests[3]);
  717. strictEqual(calls, 3, 'selects after additional segments');
  718. });
  719. test('reports an error if a segment is unreachable', function() {
  720. player.src({
  721. src: 'manifest/master.m3u8',
  722. type: 'application/vnd.apple.mpegurl'
  723. });
  724. openMediaSource(player);
  725. player.tech_.hls.bandwidth = 20000;
  726. standardXHRResponse(requests[0]); // master
  727. standardXHRResponse(requests[1]); // media
  728. requests[2].respond(400); // segment
  729. strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered');
  730. });
  731. test('updates the duration after switching playlists', function() {
  732. var selectedPlaylist = false;
  733. player.src({
  734. src: 'manifest/master.m3u8',
  735. type: 'application/vnd.apple.mpegurl'
  736. });
  737. openMediaSource(player);
  738. player.tech_.hls.bandwidth = 1e20;
  739. standardXHRResponse(requests[0]); // master
  740. standardXHRResponse(requests[1]); // media3
  741. player.tech_.hls.selectPlaylist = function() {
  742. selectedPlaylist = true;
  743. // this duration should be overwritten by the playlist change
  744. player.tech_.hls.mediaSource.duration = -Infinity;
  745. return player.tech_.hls.playlists.master.playlists[1];
  746. };
  747. standardXHRResponse(requests[2]); // segment 0
  748. standardXHRResponse(requests[3]); // media1
  749. ok(selectedPlaylist, 'selected playlist');
  750. ok(player.tech_.hls.mediaSource.duration !== -Infinity, 'updates the duration');
  751. });
  752. test('downloads additional playlists if required', function() {
  753. var
  754. called = false,
  755. playlist = {
  756. uri: 'media3.m3u8'
  757. };
  758. player.src({
  759. src: 'manifest/master.m3u8',
  760. type: 'application/vnd.apple.mpegurl'
  761. });
  762. openMediaSource(player);
  763. player.tech_.hls.bandwidth = 20000;
  764. standardXHRResponse(requests[0]);
  765. standardXHRResponse(requests[1]);
  766. // before an m3u8 is downloaded, no segments are available
  767. player.tech_.hls.selectPlaylist = function() {
  768. if (!called) {
  769. called = true;
  770. return playlist;
  771. }
  772. playlist.segments = [1, 1, 1];
  773. return playlist;
  774. };
  775. // the playlist selection is revisited after a new segment is downloaded
  776. player.trigger('timeupdate');
  777. requests[2].bandwidth = 3000000;
  778. requests[2].response = new Uint8Array([0]);
  779. requests[2].respond(200, null, '');
  780. standardXHRResponse(requests[3]);
  781. strictEqual(4, requests.length, 'requests were made');
  782. strictEqual(requests[3].url,
  783. absoluteUrl('manifest/' + playlist.uri),
  784. 'made playlist request');
  785. strictEqual(playlist.uri,
  786. player.tech_.hls.playlists.media().uri,
  787. 'a new playlists was selected');
  788. ok(player.tech_.hls.playlists.media().segments, 'segments are now available');
  789. });
  790. test('selects a playlist below the current bandwidth', function() {
  791. var playlist;
  792. player.src({
  793. src: 'manifest/master.m3u8',
  794. type: 'application/vnd.apple.mpegurl'
  795. });
  796. openMediaSource(player);
  797. standardXHRResponse(requests[0]);
  798. // the default playlist has a really high bitrate
  799. player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
  800. // playlist 1 has a very low bitrate
  801. player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1;
  802. // but the detected client bandwidth is really low
  803. player.tech_.hls.bandwidth = 10;
  804. playlist = player.tech_.hls.selectPlaylist();
  805. strictEqual(playlist,
  806. player.tech_.hls.playlists.master.playlists[1],
  807. 'the low bitrate stream is selected');
  808. });
  809. test('allows initial bandwidth to be provided', function() {
  810. player.src({
  811. src: 'manifest/master.m3u8',
  812. type: 'application/vnd.apple.mpegurl'
  813. });
  814. openMediaSource(player);
  815. player.tech_.hls.bandwidth = 500;
  816. requests[0].bandwidth = 1;
  817. requests.shift().respond(200, null,
  818. '#EXTM3U\n' +
  819. '#EXT-X-PLAYLIST-TYPE:VOD\n' +
  820. '#EXT-X-TARGETDURATION:10\n');
  821. equal(player.tech_.hls.bandwidth, 500, 'prefers user-specified intial bandwidth');
  822. });
  823. test('raises the minimum bitrate for a stream proportionially', function() {
  824. var playlist;
  825. player.src({
  826. src: 'manifest/master.m3u8',
  827. type: 'application/vnd.apple.mpegurl'
  828. });
  829. openMediaSource(player);
  830. standardXHRResponse(requests[0]);
  831. // the default playlist's bandwidth + 10% is equal to the current bandwidth
  832. player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
  833. player.tech_.hls.bandwidth = 11;
  834. // 9.9 * 1.1 < 11
  835. player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9;
  836. playlist = player.tech_.hls.selectPlaylist();
  837. strictEqual(playlist,
  838. player.tech_.hls.playlists.master.playlists[1],
  839. 'a lower bitrate stream is selected');
  840. });
  841. test('uses the lowest bitrate if no other is suitable', function() {
  842. var playlist;
  843. player.src({
  844. src: 'manifest/master.m3u8',
  845. type: 'application/vnd.apple.mpegurl'
  846. });
  847. openMediaSource(player);
  848. standardXHRResponse(requests[0]);
  849. // the lowest bitrate playlist is much greater than 1b/s
  850. player.tech_.hls.bandwidth = 1;
  851. playlist = player.tech_.hls.selectPlaylist();
  852. // playlist 1 has the lowest advertised bitrate
  853. strictEqual(playlist,
  854. player.tech_.hls.playlists.master.playlists[1],
  855. 'the lowest bitrate stream is selected');
  856. });
  857. test('selects the correct rendition by player dimensions', function() {
  858. var playlist;
  859. player.src({
  860. src: 'manifest/master.m3u8',
  861. type: 'application/vnd.apple.mpegurl'
  862. });
  863. openMediaSource(player);
  864. standardXHRResponse(requests[0]);
  865. player.width(640);
  866. player.height(360);
  867. player.tech_.hls.bandwidth = 3000000;
  868. playlist = player.tech_.hls.selectPlaylist();
  869. deepEqual(playlist.attributes.RESOLUTION, {width:960,height:540},'should return the correct resolution by player dimensions');
  870. equal(playlist.attributes.BANDWIDTH, 1928000, 'should have the expected bandwidth in case of multiple');
  871. player.width(1920);
  872. player.height(1080);
  873. player.tech_.hls.bandwidth = 3000000;
  874. playlist = player.tech_.hls.selectPlaylist();
  875. deepEqual(playlist.attributes.RESOLUTION, {
  876. width:960,
  877. height:540
  878. },'should return the correct resolution by player dimensions');
  879. equal(playlist.attributes.BANDWIDTH, 1928000, 'should have the expected bandwidth in case of multiple');
  880. player.width(396);
  881. player.height(224);
  882. playlist = player.tech_.hls.selectPlaylist();
  883. deepEqual(playlist.attributes.RESOLUTION, {
  884. width:396,
  885. height:224
  886. },'should return the correct resolution by player dimensions, if exact match');
  887. equal(playlist.attributes.BANDWIDTH, 440000, 'should have the expected bandwidth in case of multiple, if exact match');
  888. });
  889. test('selects the highest bitrate playlist when the player dimensions are ' +
  890. 'larger than any of the variants', function() {
  891. var playlist;
  892. player.src({
  893. src: 'manifest/master.m3u8',
  894. type: 'application/vnd.apple.mpegurl'
  895. });
  896. openMediaSource(player);
  897. requests.shift().respond(200, null,
  898. '#EXTM3U\n' +
  899. '#EXT-X-STREAM-INF:BANDWIDTH=1000,RESOLUTION=2x1\n' +
  900. 'media.m3u8\n' +
  901. '#EXT-X-STREAM-INF:BANDWIDTH=1,RESOLUTION=1x1\n' +
  902. 'media1.m3u8\n'); // master
  903. standardXHRResponse(requests.shift()); // media
  904. player.tech_.hls.bandwidth = 1e10;
  905. player.width(1024);
  906. player.height(768);
  907. playlist = player.tech_.hls.selectPlaylist();
  908. equal(playlist.attributes.BANDWIDTH,
  909. 1000,
  910. 'selected the highest bandwidth variant');
  911. });
  912. test('does not download the next segment if the buffer is full', function() {
  913. var currentTime = 15;
  914. player.src({
  915. src: 'manifest/media.m3u8',
  916. type: 'application/vnd.apple.mpegurl'
  917. });
  918. player.tech_.currentTime = function() {
  919. return currentTime;
  920. };
  921. player.tech_.buffered = function() {
  922. return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH);
  923. };
  924. openMediaSource(player);
  925. standardXHRResponse(requests[0]);
  926. player.trigger('timeupdate');
  927. strictEqual(requests.length, 1, 'no segment request was made');
  928. });
  929. test('downloads the next segment if the buffer is getting low', function() {
  930. player.src({
  931. src: 'manifest/media.m3u8',
  932. type: 'application/vnd.apple.mpegurl'
  933. });
  934. openMediaSource(player);
  935. standardXHRResponse(requests[0]);
  936. standardXHRResponse(requests[1]);
  937. strictEqual(requests.length, 2, 'made two requests');
  938. player.tech_.currentTime = function() {
  939. return 15;
  940. };
  941. player.tech_.buffered = function() {
  942. return videojs.createTimeRange(0, 19.999);
  943. };
  944. player.tech_.hls.sourceBuffer.trigger('updateend');
  945. player.tech_.hls.checkBuffer_();
  946. standardXHRResponse(requests[2]);
  947. strictEqual(requests.length, 3, 'made a request');
  948. strictEqual(requests[2].url,
  949. absoluteUrl('manifest/media-00002.ts'),
  950. 'made segment request');
  951. });
  952. test('buffers based on the correct TimeRange if multiple ranges exist', function() {
  953. var currentTime, buffered;
  954. player.src({
  955. src: 'manifest/media.m3u8',
  956. type: 'application/vnd.apple.mpegurl'
  957. });
  958. openMediaSource(player);
  959. player.tech_.currentTime = function() {
  960. return currentTime;
  961. };
  962. player.tech_.buffered = function() {
  963. return videojs.createTimeRange(buffered);
  964. };
  965. currentTime = 8;
  966. buffered = [[0, 10], [20, 30]];
  967. standardXHRResponse(requests[0]);
  968. standardXHRResponse(requests[1]);
  969. strictEqual(requests.length, 2, 'made two requests');
  970. strictEqual(requests[1].url,
  971. absoluteUrl('manifest/media-00002.ts'),
  972. 'made segment request');
  973. currentTime = 22;
  974. player.tech_.hls.sourceBuffer.trigger('updateend');
  975. player.tech_.hls.checkBuffer_();
  976. strictEqual(requests.length, 3, 'made three requests');
  977. strictEqual(requests[2].url,
  978. absoluteUrl('manifest/media-00003.ts'),
  979. 'made segment request');
  980. });
  981. test('stops downloading segments at the end of the playlist', function() {
  982. player.src({
  983. src: 'manifest/media.m3u8',
  984. type: 'application/vnd.apple.mpegurl'
  985. });
  986. openMediaSource(player);
  987. standardXHRResponse(requests[0]);
  988. requests = [];
  989. player.tech_.hls.mediaIndex = 4;
  990. player.trigger('timeupdate');
  991. strictEqual(requests.length, 0, 'no request is made');
  992. });
  993. test('only makes one segment request at a time', function() {
  994. player.src({
  995. src: 'manifest/media.m3u8',
  996. type: 'application/vnd.apple.mpegurl'
  997. });
  998. openMediaSource(player);
  999. standardXHRResponse(requests.pop());
  1000. player.trigger('timeupdate');
  1001. strictEqual(1, requests.length, 'one XHR is made');
  1002. player.trigger('timeupdate');
  1003. strictEqual(1, requests.length, 'only one XHR is made');
  1004. });
  1005. test('only appends one segment at a time', function() {
  1006. player.src({
  1007. src: 'manifest/media.m3u8',
  1008. type: 'application/vnd.apple.mpegurl'
  1009. });
  1010. openMediaSource(player);
  1011. standardXHRResponse(requests.pop()); // media.m3u8
  1012. standardXHRResponse(requests.pop()); // segment 0
  1013. player.tech_.hls.checkBuffer_();
  1014. equal(requests.length, 0, 'did not request while updating');
  1015. });
  1016. test('waits to download new segments until the media playlist is stable', function() {
  1017. player.src({
  1018. src: 'manifest/master.m3u8',
  1019. type: 'application/vnd.apple.mpegurl'
  1020. });
  1021. openMediaSource(player);
  1022. standardXHRResponse(requests.shift()); // master
  1023. player.tech_.hls.bandwidth = 1; // make sure we stay on the lowest variant
  1024. standardXHRResponse(requests.shift()); // media1
  1025. // force a playlist switch
  1026. player.tech_.hls.playlists.media('media3.m3u8');
  1027. standardXHRResponse(requests.shift()); // segment 0
  1028. player.tech_.hls.sourceBuffer.trigger('updateend');
  1029. equal(requests.length, 1, 'only the playlist request outstanding');
  1030. player.tech_.hls.checkBuffer_();
  1031. equal(requests.length, 1, 'delays segment fetching');
  1032. standardXHRResponse(requests.shift()); // media3
  1033. player.tech_.hls.checkBuffer_();
  1034. equal(requests.length, 1, 'resumes segment fetching');
  1035. });
  1036. test('cancels outstanding XHRs when seeking', function() {
  1037. player.src({
  1038. src: 'manifest/media.m3u8',
  1039. type: 'application/vnd.apple.mpegurl'
  1040. });
  1041. openMediaSource(player);
  1042. standardXHRResponse(requests[0]);
  1043. player.tech_.hls.media = {
  1044. segments: [{
  1045. uri: '0.ts',
  1046. duration: 10
  1047. }, {
  1048. uri: '1.ts',
  1049. duration: 10
  1050. }]
  1051. };
  1052. // attempt to seek while the download is in progress
  1053. player.currentTime(7);
  1054. clock.tick(1);
  1055. ok(requests[1].aborted, 'XHR aborted');
  1056. strictEqual(requests.length, 3, 'opened new XHR');
  1057. });
  1058. test('when outstanding XHRs are cancelled, they get aborted properly', function() {
  1059. var readystatechanges = 0;
  1060. player.src({
  1061. src: 'manifest/media.m3u8',
  1062. type: 'application/vnd.apple.mpegurl'
  1063. });
  1064. openMediaSource(player);
  1065. standardXHRResponse(requests[0]);
  1066. // trigger a segment download request
  1067. player.trigger('timeupdate');
  1068. player.tech_.hls.segmentXhr_.onreadystatechange = function() {
  1069. readystatechanges++;
  1070. };
  1071. // attempt to seek while the download is in progress
  1072. player.currentTime(12);
  1073. clock.tick(1);
  1074. ok(requests[1].aborted, 'XHR aborted');
  1075. strictEqual(requests.length, 3, 'opened new XHR');
  1076. notEqual(player.tech_.hls.segmentXhr_.url, requests[1].url, 'a new segment is request that is not the aborted one');
  1077. strictEqual(readystatechanges, 0, 'onreadystatechange was not called');
  1078. });
  1079. test('segmentXhr is properly nulled out when dispose is called', function() {
  1080. var
  1081. readystatechanges = 0,
  1082. oldDispose = Flash.prototype.dispose,
  1083. player;
  1084. Flash.prototype.dispose = function() {};
  1085. player = createPlayer();
  1086. player.src({
  1087. src: 'manifest/media.m3u8',
  1088. type: 'application/vnd.apple.mpegurl'
  1089. });
  1090. openMediaSource(player);
  1091. standardXHRResponse(requests[0]);
  1092. // trigger a segment download request
  1093. player.trigger('timeupdate');
  1094. player.tech_.hls.segmentXhr_.onreadystatechange = function() {
  1095. readystatechanges++;
  1096. };
  1097. player.tech_.hls.dispose();
  1098. ok(requests[1].aborted, 'XHR aborted');
  1099. strictEqual(requests.length, 2, 'did not open a new XHR');
  1100. equal(player.tech_.hls.segmentXhr_, null, 'the segment xhr is nulled out');
  1101. strictEqual(readystatechanges, 0, 'onreadystatechange was not called');
  1102. Flash.prototype.dispose = oldDispose;
  1103. });
  1104. test('does not modify the media index for in-buffer seeking', function() {
  1105. var mediaIndex;
  1106. player.src({
  1107. src: 'manifest/media.m3u8',
  1108. type: 'application/vnd.apple.mpegurl'
  1109. });
  1110. openMediaSource(player);
  1111. standardXHRResponse(requests.shift());
  1112. player.tech_.buffered = function() {
  1113. return videojs.createTimeRange(0, 20);
  1114. };
  1115. mediaIndex = player.tech_.hls.mediaIndex;
  1116. player.tech_.setCurrentTime(11);
  1117. clock.tick(1);
  1118. equal(player.tech_.hls.mediaIndex, mediaIndex, 'did not interrupt buffering');
  1119. equal(requests.length, 1, 'did not abort the outstanding request');
  1120. });
  1121. test('playlist 404 should end stream with a network error', function() {
  1122. player.src({
  1123. src: 'manifest/media.m3u8',
  1124. type: 'application/vnd.apple.mpegurl'
  1125. });
  1126. openMediaSource(player);
  1127. requests.pop().respond(404);
  1128. equal(player.tech_.hls.mediaSource.error_, 'network', 'set a network error');
  1129. });
  1130. test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
  1131. player.src({
  1132. src: 'manifest/media.m3u8',
  1133. type: 'application/vnd.apple.mpegurl'
  1134. });
  1135. openMediaSource(player);
  1136. standardXHRResponse(requests[0]);
  1137. requests[1].respond(404);
  1138. ok(player.tech_.hls.error.message, 'an error message is available');
  1139. equal(2, player.tech_.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
  1140. });
  1141. test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
  1142. player.src({
  1143. src: 'manifest/media.m3u8',
  1144. type: 'application/vnd.apple.mpegurl'
  1145. });
  1146. openMediaSource(player);
  1147. standardXHRResponse(requests[0]);
  1148. requests[1].respond(500);
  1149. ok(player.tech_.hls.error.message, 'an error message is available');
  1150. equal(4, player.tech_.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
  1151. });
  1152. test('seeking in an empty playlist is a non-erroring noop', function() {
  1153. var requestsLength;
  1154. player.src({
  1155. src: 'manifest/empty-live.m3u8',
  1156. type: 'application/vnd.apple.mpegurl'
  1157. });
  1158. openMediaSource(player);
  1159. requests.shift().respond(200, null, '#EXTM3U\n');
  1160. requestsLength = requests.length;
  1161. player.tech_.setCurrentTime(183);
  1162. clock.tick(1);
  1163. equal(requests.length, requestsLength, 'made no additional requests');
  1164. });
  1165. test('tech\'s duration reports Infinity for live playlists', function() {
  1166. player.src({
  1167. src: 'http://example.com/manifest/missingEndlist.m3u8',
  1168. type: 'application/vnd.apple.mpegurl'
  1169. });
  1170. openMediaSource(player);
  1171. standardXHRResponse(requests[0]);
  1172. strictEqual(player.tech_.duration(),
  1173. Infinity,
  1174. 'duration on the tech is infinity');
  1175. notEqual(player.tech_.hls.mediaSource.duration,
  1176. Infinity,
  1177. 'duration on the mediaSource is not infinity');
  1178. });
  1179. test('live playlist starts three target durations before live', function() {
  1180. var mediaPlaylist;
  1181. player.src({
  1182. src: 'live.m3u8',
  1183. type: 'application/vnd.apple.mpegurl'
  1184. });
  1185. openMediaSource(player);
  1186. requests.shift().respond(200, null,
  1187. '#EXTM3U\n' +
  1188. '#EXT-X-MEDIA-SEQUENCE:101\n' +
  1189. '#EXTINF:10,\n' +
  1190. '0.ts\n' +
  1191. '#EXTINF:10,\n' +
  1192. '1.ts\n' +
  1193. '#EXTINF:10,\n' +
  1194. '2.ts\n' +
  1195. '#EXTINF:10,\n' +
  1196. '3.ts\n' +
  1197. '#EXTINF:10,\n' +
  1198. '4.ts\n');
  1199. equal(requests.length, 0, 'no outstanding segment request');
  1200. player.tech_.paused = function() { return false; };
  1201. player.tech_.trigger('play');
  1202. clock.tick(1);
  1203. mediaPlaylist = player.tech_.hls.playlists.media();
  1204. equal(player.currentTime(), player.tech_.hls.seekable().end(0), 'seeked to the seekable end');
  1205. equal(requests.length, 1, 'begins buffering');
  1206. });
  1207. test('live playlist starts with correct currentTime value', function() {
  1208. player.src({
  1209. src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
  1210. type: 'application/vnd.apple.mpegurl'
  1211. });
  1212. openMediaSource(player);
  1213. standardXHRResponse(requests[0]);
  1214. player.tech_.hls.playlists.trigger('loadedmetadata');
  1215. player.tech_.paused = function() { return false; };
  1216. player.tech_.trigger('play');
  1217. clock.tick(1);
  1218. strictEqual(player.currentTime(),
  1219. videojs.Hls.Playlist.seekable(player.tech_.hls.playlists.media()).end(0),
  1220. 'currentTime is updated at playback');
  1221. });
  1222. test('resets the time to a seekable position when resuming a live stream ' +
  1223. 'after a long break', function() {
  1224. var seekTarget;
  1225. player.src({
  1226. src: 'live0.m3u8',
  1227. type: 'application/vnd.apple.mpegurl'
  1228. });
  1229. openMediaSource(player);
  1230. requests.shift().respond(200, null,
  1231. '#EXTM3U\n' +
  1232. '#EXT-X-MEDIA-SEQUENCE:16\n' +
  1233. '#EXTINF:10,\n' +
  1234. '16.ts\n');
  1235. // mock out the player to simulate a live stream that has been
  1236. // playing for awhile
  1237. player.tech_.hls.seekable = function() {
  1238. return videojs.createTimeRange(160, 170);
  1239. };
  1240. player.tech_.setCurrentTime = function(time) {
  1241. if (time !== undefined) {
  1242. seekTarget = time;
  1243. }
  1244. };
  1245. player.tech_.played = function() {
  1246. return videojs.createTimeRange(120, 170);
  1247. };
  1248. player.tech_.trigger('playing');
  1249. player.tech_.trigger('play');
  1250. equal(seekTarget, player.seekable().start(0), 'seeked to the start of seekable');
  1251. player.tech_.trigger('seeked');
  1252. });
  1253. test('reloads out-of-date live playlists when switching variants', function() {
  1254. player.src({
  1255. src: 'http://example.com/master.m3u8',
  1256. type: 'application/vnd.apple.mpegurl'
  1257. });
  1258. openMediaSource(player);
  1259. player.tech_.hls.master = {
  1260. playlists: [{
  1261. mediaSequence: 15,
  1262. segments: [1, 1, 1]
  1263. }, {
  1264. uri: 'http://example.com/variant-update.m3u8',
  1265. mediaSequence: 0,
  1266. segments: [1, 1]
  1267. }]
  1268. };
  1269. // playing segment 15 on playlist zero
  1270. player.tech_.hls.media = player.tech_.hls.master.playlists[0];
  1271. player.mediaIndex = 1;
  1272. window.manifests['variant-update'] = '#EXTM3U\n' +
  1273. '#EXT-X-MEDIA-SEQUENCE:16\n' +
  1274. '#EXTINF:10,\n' +
  1275. '16.ts\n' +
  1276. '#EXTINF:10,\n' +
  1277. '17.ts\n';
  1278. // switch playlists
  1279. player.tech_.hls.selectPlaylist = function() {
  1280. return player.tech_.hls.master.playlists[1];
  1281. };
  1282. // timeupdate downloads segment 16 then switches playlists
  1283. player.trigger('timeupdate');
  1284. strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment');
  1285. });
  1286. test('if withCredentials global option is used, withCredentials is set on the XHR object', function() {
  1287. player.dispose();
  1288. videojs.options.hls = {
  1289. withCredentials: true
  1290. };
  1291. player = createPlayer();
  1292. player.src({
  1293. src: 'http://example.com/media.m3u8',
  1294. type: 'application/vnd.apple.mpegurl'
  1295. });
  1296. openMediaSource(player);
  1297. ok(requests[0].withCredentials,
  1298. 'with credentials should be set to true if that option is passed in');
  1299. });
  1300. test('if withCredentials src option is used, withCredentials is set on the XHR object', function() {
  1301. player.dispose();
  1302. player = createPlayer();
  1303. player.src({
  1304. src: 'http://example.com/media.m3u8',
  1305. type: 'application/vnd.apple.mpegurl',
  1306. withCredentials: true
  1307. });
  1308. openMediaSource(player);
  1309. ok(requests[0].withCredentials,
  1310. 'with credentials should be set to true if that option is passed in');
  1311. });
  1312. test('src level credentials supersede the global options', function() {
  1313. player.dispose();
  1314. player = createPlayer();
  1315. player.src({
  1316. src: 'http://example.com/media.m3u8',
  1317. type: 'application/vnd.apple.mpegurl',
  1318. withCredentials: true
  1319. });
  1320. openMediaSource(player);
  1321. ok(requests[0].withCredentials,
  1322. 'with credentials should be set to true if that option is passed in');
  1323. });
  1324. test('does not break if the playlist has no segments', function() {
  1325. player.src({
  1326. src: 'manifest/master.m3u8',
  1327. type: 'application/vnd.apple.mpegurl'
  1328. });
  1329. try {
  1330. openMediaSource(player);
  1331. requests[0].respond(200, null,
  1332. '#EXTM3U\n' +
  1333. '#EXT-X-PLAYLIST-TYPE:VOD\n' +
  1334. '#EXT-X-TARGETDURATION:10\n');
  1335. } catch(e) {
  1336. ok(false, 'an error was thrown');
  1337. throw e;
  1338. }
  1339. ok(true, 'no error was thrown');
  1340. strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
  1341. });
  1342. test('aborts segment processing on seek', function() {
  1343. var currentTime = 0;
  1344. player.src({
  1345. src: 'discontinuity.m3u8',
  1346. type: 'application/vnd.apple.mpegurl'
  1347. });
  1348. openMediaSource(player);
  1349. player.tech_.currentTime = function() {
  1350. return currentTime;
  1351. };
  1352. player.tech_.buffered = function() {
  1353. return videojs.createTimeRange();
  1354. };
  1355. requests.shift().respond(200, null,
  1356. '#EXTM3U\n' +
  1357. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1358. '#EXTINF:10,0\n' +
  1359. '1.ts\n' +
  1360. '#EXT-X-DISCONTINUITY\n' +
  1361. '#EXTINF:10,0\n' +
  1362. '2.ts\n' +
  1363. '#EXT-X-ENDLIST\n'); // media
  1364. standardXHRResponse(requests.shift()); // 1.ts
  1365. standardXHRResponse(requests.shift()); // key.php
  1366. ok(player.tech_.hls.pendingSegment_, 'decrypting the segment');
  1367. // seek back to the beginning
  1368. player.currentTime(0);
  1369. clock.tick(1);
  1370. ok(!player.tech_.hls.pendingSegment_, 'aborted processing');
  1371. });
  1372. test('calls mediaSource\'s timestampOffset on discontinuity', function() {
  1373. var buffered = [[]];
  1374. player.src({
  1375. src: 'discontinuity.m3u8',
  1376. type: 'application/vnd.apple.mpegurl'
  1377. });
  1378. openMediaSource(player);
  1379. player.play();
  1380. player.tech_.buffered = function() {
  1381. return videojs.createTimeRange(buffered);
  1382. };
  1383. requests.shift().respond(200, null,
  1384. '#EXTM3U\n' +
  1385. '#EXTINF:10,0\n' +
  1386. '1.ts\n' +
  1387. '#EXT-X-DISCONTINUITY\n' +
  1388. '#EXTINF:10,0\n' +
  1389. '2.ts\n' +
  1390. '#EXT-X-ENDLIST\n');
  1391. player.tech_.hls.sourceBuffer.timestampOffset = 0;
  1392. standardXHRResponse(requests.shift()); // 1.ts
  1393. equal(player.tech_.hls.sourceBuffer.timestampOffset,
  1394. 0,
  1395. 'timestampOffset starts at zero');
  1396. buffered = [[0, 10]];
  1397. player.tech_.hls.sourceBuffer.trigger('updateend');
  1398. standardXHRResponse(requests.shift()); // 2.ts
  1399. equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity');
  1400. });
  1401. test('sets timestampOffset when seeking with discontinuities', function() {
  1402. var timeRange = videojs.createTimeRange(0, 10);
  1403. player.src({
  1404. src: 'discontinuity.m3u8',
  1405. type: 'application/vnd.apple.mpegurl'
  1406. });
  1407. openMediaSource(player);
  1408. player.play();
  1409. player.tech_.buffered = function() {
  1410. return timeRange;
  1411. };
  1412. player.tech_.seeking = function (){
  1413. return true;
  1414. };
  1415. requests.pop().respond(200, null,
  1416. '#EXTM3U\n' +
  1417. '#EXTINF:10,0\n' +
  1418. '1.ts\n' +
  1419. '#EXTINF:10,0\n' +
  1420. '2.ts\n' +
  1421. '#EXT-X-DISCONTINUITY\n' +
  1422. '#EXTINF:10,0\n' +
  1423. '3.ts\n' +
  1424. '#EXT-X-ENDLIST\n');
  1425. player.tech_.hls.sourceBuffer.timestampOffset = 0;
  1426. player.currentTime(21);
  1427. clock.tick(1);
  1428. equal(requests.shift().aborted, true, 'aborted first request');
  1429. standardXHRResponse(requests.pop()); // 3.ts
  1430. clock.tick(1000);
  1431. equal(player.tech_.hls.sourceBuffer.timestampOffset, 20, 'timestampOffset starts at zero');
  1432. });
  1433. test('can seek before the source buffer opens', function() {
  1434. player.src({
  1435. src: 'media.m3u8',
  1436. type: 'application/vnd.apple.mpegurl'
  1437. });
  1438. player.tech_.triggerReady();
  1439. clock.tick(1);
  1440. standardXHRResponse(requests.shift());
  1441. player.triggerReady();
  1442. player.currentTime(1);
  1443. equal(player.currentTime(), 1, 'seeked');
  1444. });
  1445. QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() {
  1446. var bufferEnd;
  1447. player.src({
  1448. src: 'discontinuity.m3u8',
  1449. type: 'application/vnd.apple.mpegurl'
  1450. });
  1451. openMediaSource(player);
  1452. player.tech_.buffered = function() {
  1453. return videojs.createTimeRange(0, bufferEnd);
  1454. };
  1455. requests.pop().respond(200, null,
  1456. '#EXTM3U\n' +
  1457. '#EXTINF:10,0\n' +
  1458. '1.ts\n' +
  1459. '#EXT-X-DISCONTINUITY\n' +
  1460. '#EXTINF:10,0\n' +
  1461. '2.ts\n' +
  1462. '#EXT-X-ENDLIST\n');
  1463. standardXHRResponse(requests.pop()); // 1.ts
  1464. // seek to a discontinuity
  1465. player.tech_.setCurrentTime(10);
  1466. bufferEnd = 9.9;
  1467. clock.tick(1);
  1468. standardXHRResponse(requests.pop()); // 1.ts, again
  1469. player.tech_.hls.checkBuffer_();
  1470. standardXHRResponse(requests.pop()); // 2.ts
  1471. equal(player.tech_.hls.sourceBuffer.timestampOffset,
  1472. 9.9,
  1473. 'set the timestamp offset');
  1474. });
  1475. test('tracks segment end times as they are buffered', function() {
  1476. var bufferEnd = 0;
  1477. player.src({
  1478. src: 'media.m3u8',
  1479. type: 'application/x-mpegURL'
  1480. });
  1481. openMediaSource(player);
  1482. // as new segments are downloaded, the buffer end is updated
  1483. player.tech_.buffered = function() {
  1484. return videojs.createTimeRange(0, bufferEnd);
  1485. };
  1486. requests.shift().respond(200, null,
  1487. '#EXTM3U\n' +
  1488. '#EXTINF:10,\n' +
  1489. '0.ts\n' +
  1490. '#EXTINF:10,\n' +
  1491. '1.ts\n' +
  1492. '#EXT-X-ENDLIST\n');
  1493. // 0.ts is shorter than advertised
  1494. standardXHRResponse(requests.shift());
  1495. equal(player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8');
  1496. bufferEnd = 9.5;
  1497. player.tech_.hls.sourceBuffer.trigger('update');
  1498. player.tech_.hls.sourceBuffer.trigger('updateend');
  1499. equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration');
  1500. });
  1501. QUnit.skip('seeking does not fail when targeted between segments', function() {
  1502. var currentTime, segmentUrl;
  1503. player.src({
  1504. src: 'media.m3u8',
  1505. type: 'application/vnd.apple.mpegurl'
  1506. });
  1507. openMediaSource(player);
  1508. // mock out the currentTime callbacks
  1509. player.tech_.el().vjs_setProperty = function(property, value) {
  1510. if (property === 'currentTime') {
  1511. currentTime = value;
  1512. }
  1513. };
  1514. player.tech_.el().vjs_getProperty = function(property) {
  1515. if (property === 'currentTime') {
  1516. return currentTime;
  1517. }
  1518. };
  1519. standardXHRResponse(requests.shift()); // media
  1520. standardXHRResponse(requests.shift()); // segment 0
  1521. player.tech_.hls.checkBuffer_();
  1522. segmentUrl = requests[0].url;
  1523. standardXHRResponse(requests.shift()); // segment 1
  1524. // seek to a time that is greater than the last tag in segment 0 but
  1525. // less than the first in segment 1
  1526. // FIXME: it's not possible to seek here without timestamp-based
  1527. // segment durations
  1528. player.tech_.setCurrentTime(9.4);
  1529. clock.tick(1);
  1530. equal(requests[0].url, segmentUrl, 'requested the later segment');
  1531. standardXHRResponse(requests.shift()); // segment 1
  1532. player.tech_.trigger('seeked');
  1533. equal(player.currentTime(), 9.5, 'seeked to the later time');
  1534. });
  1535. test('resets the switching algorithm if a request times out', function() {
  1536. player.src({
  1537. src: 'master.m3u8',
  1538. type: 'application/vnd.apple.mpegurl'
  1539. });
  1540. openMediaSource(player);
  1541. player.tech_.hls.bandwidth = 1e20;
  1542. standardXHRResponse(requests.shift()); // master
  1543. standardXHRResponse(requests.shift()); // media.m3u8
  1544. // simulate a segment timeout
  1545. requests[0].timedout = true;
  1546. requests.shift().abort();
  1547. standardXHRResponse(requests.shift());
  1548. strictEqual(player.tech_.hls.playlists.media(),
  1549. player.tech_.hls.playlists.master.playlists[1],
  1550. 'reset to the lowest bitrate playlist');
  1551. });
  1552. test('disposes the playlist loader', function() {
  1553. var disposes = 0, player, loaderDispose;
  1554. player = createPlayer();
  1555. player.src({
  1556. src: 'manifest/master.m3u8',
  1557. type: 'application/vnd.apple.mpegurl'
  1558. });
  1559. openMediaSource(player);
  1560. loaderDispose = player.tech_.hls.playlists.dispose;
  1561. player.tech_.hls.playlists.dispose = function() {
  1562. disposes++;
  1563. loaderDispose.call(player.tech_.hls.playlists);
  1564. };
  1565. player.dispose();
  1566. strictEqual(disposes, 1, 'disposed playlist loader');
  1567. });
  1568. test('remove event handlers on dispose', function() {
  1569. var
  1570. player,
  1571. unscoped = 0;
  1572. player = createPlayer();
  1573. player.on = function(owner) {
  1574. if (typeof owner !== 'object') {
  1575. unscoped++;
  1576. }
  1577. };
  1578. player.off = function(owner) {
  1579. if (typeof owner !== 'object') {
  1580. unscoped--;
  1581. }
  1582. };
  1583. player.src({
  1584. src: 'manifest/master.m3u8',
  1585. type: 'application/vnd.apple.mpegurl'
  1586. });
  1587. openMediaSource(player);
  1588. standardXHRResponse(requests[0]);
  1589. standardXHRResponse(requests[1]);
  1590. player.dispose();
  1591. ok(unscoped <= 0, 'no unscoped handlers');
  1592. });
  1593. test('aborts the source buffer on disposal', function() {
  1594. var aborts = 0, player;
  1595. player = createPlayer();
  1596. player.src({
  1597. src: 'manifest/master.m3u8',
  1598. type: 'application/vnd.apple.mpegurl'
  1599. });
  1600. openMediaSource(player);
  1601. player.dispose();
  1602. ok(true, 'disposed before creating the source buffer');
  1603. requests.length = 0;
  1604. player = createPlayer();
  1605. player.src({
  1606. src: 'manifest/media.m3u8',
  1607. type: 'application/vnd.apple.mpegurl'
  1608. });
  1609. openMediaSource(player);
  1610. standardXHRResponse(requests.shift());
  1611. player.tech_.hls.sourceBuffer.abort = function() {
  1612. aborts++;
  1613. };
  1614. player.dispose();
  1615. strictEqual(aborts, 1, 'aborted the source buffer');
  1616. });
  1617. test('the source handler supports HLS mime types', function() {
  1618. ['html5', 'flash'].forEach(function(techName) {
  1619. ok(videojs.HlsSourceHandler(techName).canHandleSource({
  1620. type: 'aPplicatiOn/x-MPegUrl'
  1621. }), 'supports x-mpegurl');
  1622. ok(videojs.HlsSourceHandler(techName).canHandleSource({
  1623. type: 'aPplicatiOn/VnD.aPPle.MpEgUrL'
  1624. }), 'supports vnd.apple.mpegurl');
  1625. ok(!(videojs.HlsSourceHandler(techName).canHandleSource({
  1626. type: 'video/mp4'
  1627. }) instanceof videojs.Hls), 'does not support mp4');
  1628. ok(!(videojs.HlsSourceHandler(techName).canHandleSource({
  1629. type: 'video/x-flv'
  1630. }) instanceof videojs.Hls), 'does not support flv');
  1631. });
  1632. });
  1633. test('fires loadstart manually if Flash is used', function() {
  1634. var
  1635. tech = new (videojs.extend(videojs.EventTarget, {
  1636. buffered: function() {
  1637. return videojs.createTimeRange();
  1638. },
  1639. currentTime: function() {
  1640. return 0;
  1641. },
  1642. el: function() {
  1643. return {};
  1644. },
  1645. preload: function() {
  1646. return 'auto';
  1647. },
  1648. src: function() {},
  1649. setTimeout: window.setTimeout
  1650. }))(),
  1651. loadstarts = 0;
  1652. tech.on('loadstart', function() {
  1653. loadstarts++;
  1654. });
  1655. videojs.HlsSourceHandler('flash').handleSource({
  1656. src: 'movie.m3u8',
  1657. type: 'application/x-mpegURL'
  1658. }, tech);
  1659. equal(loadstarts, 0, 'loadstart is not synchronous');
  1660. clock.tick(1);
  1661. equal(loadstarts, 1, 'fired loadstart');
  1662. });
  1663. test('has no effect if native HLS is available', function() {
  1664. var player;
  1665. videojs.Hls.supportsNativeHls = true;
  1666. player = createPlayer();
  1667. player.src({
  1668. src: 'http://example.com/manifest/master.m3u8',
  1669. type: 'application/x-mpegURL'
  1670. });
  1671. ok(!player.tech_.hls, 'did not load hls tech');
  1672. player.dispose();
  1673. });
  1674. test('is not supported on browsers without typed arrays', function() {
  1675. var oldArray = window.Uint8Array;
  1676. window.Uint8Array = null;
  1677. ok(!videojs.Hls.isSupported(), 'HLS is not supported');
  1678. // cleanup
  1679. window.Uint8Array = oldArray;
  1680. });
  1681. test('tracks the bytes downloaded', function() {
  1682. player.src({
  1683. src: 'http://example.com/media.m3u8',
  1684. type: 'application/vnd.apple.mpegurl'
  1685. });
  1686. openMediaSource(player);
  1687. strictEqual(player.tech_.hls.bytesReceived, 0, 'no bytes received');
  1688. requests.shift().respond(200, null,
  1689. '#EXTM3U\n' +
  1690. '#EXTINF:10,\n' +
  1691. '0.ts\n' +
  1692. '#EXTINF:10,\n' +
  1693. '1.ts\n' +
  1694. '#EXT-X-ENDLIST\n');
  1695. // transmit some segment bytes
  1696. requests[0].response = new ArrayBuffer(17);
  1697. requests.shift().respond(200, null, '');
  1698. player.tech_.hls.sourceBuffer.trigger('updateend');
  1699. strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received');
  1700. player.tech_.hls.checkBuffer_();
  1701. // transmit some more
  1702. requests[0].response = new ArrayBuffer(5);
  1703. requests.shift().respond(200, null, '');
  1704. strictEqual(player.tech_.hls.bytesReceived, 22, 'tracked more bytes');
  1705. });
  1706. test('re-emits mediachange events', function() {
  1707. var mediaChanges = 0;
  1708. player.on('mediachange', function() {
  1709. mediaChanges++;
  1710. });
  1711. player.src({
  1712. src: 'http://example.com/media.m3u8',
  1713. type: 'application/vnd.apple.mpegurl'
  1714. });
  1715. openMediaSource(player);
  1716. player.tech_.hls.playlists.trigger('mediachange');
  1717. strictEqual(mediaChanges, 1, 'fired mediachange');
  1718. });
  1719. test('can be disposed before finishing initialization', function() {
  1720. var readyHandlers = [];
  1721. player.ready = function(callback) {
  1722. readyHandlers.push(callback);
  1723. };
  1724. player.src({
  1725. src: 'http://example.com/media.m3u8',
  1726. type: 'application/vnd.apple.mpegurl'
  1727. });
  1728. player.src({
  1729. src: 'http://example.com/media.mp4',
  1730. type: 'video/mp4'
  1731. });
  1732. ok(readyHandlers.length > 0, 'registered a ready handler');
  1733. try {
  1734. while (readyHandlers.length) {
  1735. readyHandlers.shift().call(player);
  1736. openMediaSource(player);
  1737. }
  1738. ok(true, 'did not throw an exception');
  1739. } catch (e) {
  1740. ok(false, 'threw an exception');
  1741. }
  1742. });
  1743. test('calls ended() on the media source at the end of a playlist', function() {
  1744. var endOfStreams = 0, buffered = [[]];
  1745. player.src({
  1746. src: 'http://example.com/media.m3u8',
  1747. type: 'application/vnd.apple.mpegurl'
  1748. });
  1749. openMediaSource(player);
  1750. player.tech_.buffered = function() {
  1751. return videojs.createTimeRanges(buffered);
  1752. };
  1753. player.tech_.hls.mediaSource.endOfStream = function() {
  1754. endOfStreams++;
  1755. };
  1756. // playlist response
  1757. requests.shift().respond(200, null,
  1758. '#EXTM3U\n' +
  1759. '#EXTINF:10,\n' +
  1760. '0.ts\n' +
  1761. '#EXT-X-ENDLIST\n');
  1762. // segment response
  1763. requests[0].response = new ArrayBuffer(17);
  1764. requests.shift().respond(200, null, '');
  1765. strictEqual(endOfStreams, 0, 'waits for the buffer update to finish');
  1766. buffered =[[0, 10]];
  1767. player.tech_.hls.sourceBuffer.trigger('updateend');
  1768. strictEqual(endOfStreams, 1, 'ended media source');
  1769. });
  1770. test('calling play() at the end of a video replays', function() {
  1771. var seekTime = -1;
  1772. player.src({
  1773. src: 'http://example.com/media.m3u8',
  1774. type: 'application/vnd.apple.mpegurl'
  1775. });
  1776. openMediaSource(player);
  1777. player.tech_.setCurrentTime = function(time) {
  1778. if (time !== undefined) {
  1779. seekTime = time;
  1780. }
  1781. return 0;
  1782. };
  1783. requests.shift().respond(200, null,
  1784. '#EXTM3U\n' +
  1785. '#EXTINF:10,\n' +
  1786. '0.ts\n' +
  1787. '#EXT-X-ENDLIST\n');
  1788. standardXHRResponse(requests.shift());
  1789. player.tech_.ended = function() {
  1790. return true;
  1791. };
  1792. player.tech_.trigger('play');
  1793. equal(seekTime, 0, 'seeked to the beginning');
  1794. });
  1795. test('segments remain pending without a source buffer', function() {
  1796. player.src({
  1797. src: 'https://example.com/encrypted-media.m3u8',
  1798. type: 'application/vnd.apple.mpegurl'
  1799. });
  1800. openMediaSource(player);
  1801. requests.shift().respond(200, null,
  1802. '#EXTM3U\n' +
  1803. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=52"\n' +
  1804. '#EXTINF:10,\n' +
  1805. 'http://media.example.com/fileSequence52-A.ts' +
  1806. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=53"\n' +
  1807. '#EXTINF:10,\n' +
  1808. 'http://media.example.com/fileSequence53-B.ts\n' +
  1809. '#EXT-X-ENDLIST\n');
  1810. player.tech_.hls.sourceBuffer = undefined;
  1811. standardXHRResponse(requests.shift()); // key
  1812. standardXHRResponse(requests.shift()); // segment
  1813. player.tech_.hls.checkBuffer_();
  1814. ok(player.tech_.hls.pendingSegment_, 'waiting for the source buffer');
  1815. });
  1816. test('keys are requested when an encrypted segment is loaded', function() {
  1817. player.src({
  1818. src: 'https://example.com/encrypted.m3u8',
  1819. type: 'application/vnd.apple.mpegurl'
  1820. });
  1821. openMediaSource(player);
  1822. player.tech_.trigger('play');
  1823. standardXHRResponse(requests.shift()); // playlist
  1824. strictEqual(requests.length, 2, 'a key XHR is created');
  1825. strictEqual(requests[0].url,
  1826. player.tech_.hls.playlists.media().segments[0].key.uri,
  1827. 'key XHR is created with correct uri');
  1828. strictEqual(requests[1].url,
  1829. player.tech_.hls.playlists.media().segments[0].uri,
  1830. 'segment XHR is created with correct uri');
  1831. });
  1832. test('keys are resolved relative to the master playlist', function() {
  1833. player.src({
  1834. src: 'video/master-encrypted.m3u8',
  1835. type: 'application/vnd.apple.mpegurl'
  1836. });
  1837. openMediaSource(player);
  1838. requests.shift().respond(200, null,
  1839. '#EXTM3U\n' +
  1840. '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
  1841. 'playlist/playlist.m3u8\n' +
  1842. '#EXT-X-ENDLIST\n');
  1843. requests.shift().respond(200, null,
  1844. '#EXTM3U\n' +
  1845. '#EXT-X-TARGETDURATION:15\n' +
  1846. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1847. '#EXTINF:2.833,\n' +
  1848. 'http://media.example.com/fileSequence1.ts\n' +
  1849. '#EXT-X-ENDLIST\n');
  1850. equal(requests.length, 2, 'requested the key');
  1851. equal(requests[0].url,
  1852. absoluteUrl('video/playlist/keys/key.php'),
  1853. 'resolves multiple relative paths');
  1854. });
  1855. test('keys are resolved relative to their containing playlist', function() {
  1856. player.src({
  1857. src: 'video/media-encrypted.m3u8',
  1858. type: 'application/vnd.apple.mpegurl'
  1859. });
  1860. openMediaSource(player);
  1861. requests.shift().respond(200, null,
  1862. '#EXTM3U\n' +
  1863. '#EXT-X-TARGETDURATION:15\n' +
  1864. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1865. '#EXTINF:2.833,\n' +
  1866. 'http://media.example.com/fileSequence1.ts\n' +
  1867. '#EXT-X-ENDLIST\n');
  1868. equal(requests.length, 2, 'requested a key');
  1869. equal(requests[0].url,
  1870. absoluteUrl('video/keys/key.php'),
  1871. 'resolves multiple relative paths');
  1872. });
  1873. test('a new key XHR is created when a the segment is requested', function() {
  1874. player.src({
  1875. src: 'https://example.com/encrypted-media.m3u8',
  1876. type: 'application/vnd.apple.mpegurl'
  1877. });
  1878. openMediaSource(player);
  1879. requests.shift().respond(200, null,
  1880. '#EXTM3U\n' +
  1881. '#EXT-X-TARGETDURATION:15\n' +
  1882. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1883. '#EXTINF:2.833,\n' +
  1884. 'http://media.example.com/fileSequence1.ts\n' +
  1885. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
  1886. '#EXTINF:2.833,\n' +
  1887. 'http://media.example.com/fileSequence2.ts\n' +
  1888. '#EXT-X-ENDLIST\n');
  1889. standardXHRResponse(requests.shift()); // key 1
  1890. standardXHRResponse(requests.shift()); // segment 1
  1891. // "finish" decrypting segment 1
  1892. player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16);
  1893. player.tech_.hls.checkBuffer_();
  1894. player.tech_.buffered = function() {
  1895. return videojs.createTimeRange(0, 2.833);
  1896. };
  1897. player.tech_.hls.sourceBuffer.trigger('updateend');
  1898. strictEqual(requests.length, 2, 'a key XHR is created');
  1899. strictEqual(requests[0].url,
  1900. 'https://example.com/' +
  1901. player.tech_.hls.playlists.media().segments[1].key.uri,
  1902. 'a key XHR is created with the correct uri');
  1903. });
  1904. test('seeking should abort an outstanding key request and create a new one', function() {
  1905. player.src({
  1906. src: 'https://example.com/encrypted.m3u8',
  1907. type: 'application/vnd.apple.mpegurl'
  1908. });
  1909. openMediaSource(player);
  1910. requests.shift().respond(200, null,
  1911. '#EXTM3U\n' +
  1912. '#EXT-X-TARGETDURATION:15\n' +
  1913. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1914. '#EXTINF:9,\n' +
  1915. 'http://media.example.com/fileSequence1.ts\n' +
  1916. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
  1917. '#EXTINF:9,\n' +
  1918. 'http://media.example.com/fileSequence2.ts\n' +
  1919. '#EXT-X-ENDLIST\n');
  1920. standardXHRResponse(requests.pop()); // segment 1
  1921. player.currentTime(11);
  1922. clock.tick(1);
  1923. ok(requests[0].aborted, 'the key XHR should be aborted');
  1924. requests.shift(); // aborted key 1
  1925. equal(requests.length, 2, 'requested the new key');
  1926. equal(requests[0].url,
  1927. 'https://example.com/' +
  1928. player.tech_.hls.playlists.media().segments[1].key.uri,
  1929. 'urls should match');
  1930. });
  1931. test('retries key requests once upon failure', function() {
  1932. player.src({
  1933. src: 'https://example.com/encrypted.m3u8',
  1934. type: 'application/vnd.apple.mpegurl'
  1935. });
  1936. openMediaSource(player);
  1937. player.tech_.trigger('play');
  1938. requests.shift().respond(200, null,
  1939. '#EXTM3U\n' +
  1940. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
  1941. '#EXTINF:2.833,\n' +
  1942. 'http://media.example.com/fileSequence52-A.ts\n' +
  1943. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
  1944. '#EXTINF:15.0,\n' +
  1945. 'http://media.example.com/fileSequence53-A.ts\n');
  1946. standardXHRResponse(requests.pop()); // segment
  1947. requests[0].respond(404);
  1948. equal(requests.length, 2, 'create a new XHR for the same key');
  1949. equal(requests[1].url, requests[0].url, 'should be the same key');
  1950. requests[1].respond(404);
  1951. equal(requests.length, 2, 'gives up after one retry');
  1952. });
  1953. test('errors if key requests fail more than once', function() {
  1954. var bytes = [];
  1955. player.src({
  1956. src: 'https://example.com/encrypted-media.m3u8',
  1957. type: 'application/vnd.apple.mpegurl'
  1958. });
  1959. openMediaSource(player);
  1960. player.tech_.trigger('play');
  1961. requests.shift().respond(200, null,
  1962. '#EXTM3U\n' +
  1963. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
  1964. '#EXTINF:2.833,\n' +
  1965. 'http://media.example.com/fileSequence52-A.ts\n' +
  1966. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
  1967. '#EXTINF:15.0,\n' +
  1968. 'http://media.example.com/fileSequence53-A.ts\n');
  1969. player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) {
  1970. bytes.push(chunk);
  1971. };
  1972. standardXHRResponse(requests.pop()); // segment 1
  1973. requests.shift().respond(404); // fail key
  1974. requests.shift().respond(404); // fail key, again
  1975. player.tech_.hls.checkBuffer_();
  1976. equal(player.tech_.hls.mediaSource.error_,
  1977. 'network',
  1978. 'triggered a network error');
  1979. });
  1980. test('the key is supplied to the decrypter in the correct format', function() {
  1981. var keys = [];
  1982. player.src({
  1983. src: 'https://example.com/encrypted-media.m3u8',
  1984. type: 'application/vnd.apple.mpegurl'
  1985. });
  1986. openMediaSource(player);
  1987. player.tech_.trigger('play');
  1988. requests.pop().respond(200, null,
  1989. '#EXTM3U\n' +
  1990. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  1991. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
  1992. '#EXTINF:2.833,\n' +
  1993. 'http://media.example.com/fileSequence52-A.ts\n' +
  1994. '#EXTINF:15.0,\n' +
  1995. 'http://media.example.com/fileSequence52-B.ts\n');
  1996. videojs.Hls.Decrypter = function(encrypted, key) {
  1997. keys.push(key);
  1998. };
  1999. standardXHRResponse(requests.pop()); // segment
  2000. requests[0].response = new Uint32Array([0,1,2,3]).buffer;
  2001. requests[0].respond(200, null, '');
  2002. requests.shift(); // key
  2003. equal(keys.length, 1, 'only one Decrypter was constructed');
  2004. deepEqual(keys[0],
  2005. new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
  2006. 'passed the specified segment key');
  2007. });
  2008. test('supplies the media sequence of current segment as the IV by default, if no IV is specified', function() {
  2009. var ivs = [];
  2010. player.src({
  2011. src: 'https://example.com/encrypted-media.m3u8',
  2012. type: 'application/vnd.apple.mpegurl'
  2013. });
  2014. openMediaSource(player);
  2015. player.tech_.trigger('play');
  2016. requests.pop().respond(200, null,
  2017. '#EXTM3U\n' +
  2018. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  2019. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
  2020. '#EXTINF:2.833,\n' +
  2021. 'http://media.example.com/fileSequence52-A.ts\n' +
  2022. '#EXTINF:15.0,\n' +
  2023. 'http://media.example.com/fileSequence52-B.ts\n');
  2024. videojs.Hls.Decrypter = function(encrypted, key, iv) {
  2025. ivs.push(iv);
  2026. };
  2027. requests[0].response = new Uint32Array([0,0,0,0]).buffer;
  2028. requests[0].respond(200, null, '');
  2029. requests.shift();
  2030. standardXHRResponse(requests.pop());
  2031. equal(ivs.length, 1, 'only one Decrypter was constructed');
  2032. deepEqual(ivs[0],
  2033. new Uint32Array([0, 0, 0, 5]),
  2034. 'the IV for the segment is the media sequence');
  2035. });
  2036. test('switching playlists with an outstanding key request does not stall playback', function() {
  2037. var buffered = [];
  2038. var media = '#EXTM3U\n' +
  2039. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  2040. '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
  2041. '#EXTINF:2.833,\n' +
  2042. 'http://media.example.com/fileSequence52-A.ts\n' +
  2043. '#EXTINF:15.0,\n' +
  2044. 'http://media.example.com/fileSequence52-B.ts\n';
  2045. player.src({
  2046. src: 'https://example.com/master.m3u8',
  2047. type: 'application/vnd.apple.mpegurl'
  2048. });
  2049. openMediaSource(player);
  2050. player.tech_.trigger('play');
  2051. player.tech_.hls.bandwidth = 1;
  2052. player.tech_.buffered = function() {
  2053. return videojs.createTimeRange(buffered);
  2054. };
  2055. // master playlist
  2056. standardXHRResponse(requests.shift());
  2057. // media playlist
  2058. requests.shift().respond(200, null, media);
  2059. // mock out media switching from this point on
  2060. player.tech_.hls.playlists.media = function() {
  2061. return player.tech_.hls.playlists.master.playlists[1];
  2062. };
  2063. // first segment of the original media playlist
  2064. standardXHRResponse(requests.pop());
  2065. // "switch" media
  2066. player.tech_.hls.playlists.trigger('mediachange');
  2067. ok(!requests[0].aborted, 'did not abort the key request');
  2068. // "finish" decrypting segment 1
  2069. standardXHRResponse(requests.shift()); // key
  2070. player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16);
  2071. player.tech_.hls.checkBuffer_();
  2072. buffered = [[0, 2.833]];
  2073. player.tech_.hls.sourceBuffer.trigger('updateend');
  2074. player.tech_.hls.checkBuffer_();
  2075. equal(requests.length, 1, 'made a request');
  2076. equal(requests[0].url,
  2077. 'http://media.example.com/fileSequence52-B.ts',
  2078. 'requested the segment');
  2079. });
  2080. test('resolves relative key URLs against the playlist', function() {
  2081. player.src({
  2082. src: 'https://example.com/media.m3u8',
  2083. type: 'application/vnd.apple.mpegurl'
  2084. });
  2085. openMediaSource(player);
  2086. requests.shift().respond(200, null,
  2087. '#EXTM3U\n' +
  2088. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  2089. '#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' +
  2090. '#EXTINF:2.833,\n' +
  2091. 'http://media.example.com/fileSequence52-A.ts\n' +
  2092. '#EXT-X-ENDLIST\n');
  2093. equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL');
  2094. });
  2095. test('treats invalid keys as a key request failure', function() {
  2096. var bytes = [];
  2097. player.src({
  2098. src: 'https://example.com/media.m3u8',
  2099. type: 'application/vnd.apple.mpegurl'
  2100. });
  2101. openMediaSource(player);
  2102. player.tech_.trigger('play');
  2103. requests.shift().respond(200, null,
  2104. '#EXTM3U\n' +
  2105. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  2106. '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
  2107. '#EXTINF:2.833,\n' +
  2108. 'http://media.example.com/fileSequence52-A.ts\n' +
  2109. '#EXT-X-KEY:METHOD=NONE\n' +
  2110. '#EXTINF:15.0,\n' +
  2111. 'http://media.example.com/fileSequence52-B.ts\n');
  2112. player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) {
  2113. bytes.push(chunk);
  2114. };
  2115. // segment request
  2116. standardXHRResponse(requests.pop());
  2117. // keys should be 16 bytes long
  2118. requests[0].response = new Uint8Array(1).buffer;
  2119. requests.shift().respond(200, null, '');
  2120. equal(requests[0].url, 'https://priv.example.com/key.php?r=52', 'retries the key');
  2121. // the retried response is invalid, too
  2122. requests[0].response = new Uint8Array(1);
  2123. requests.shift().respond(200, null, '');
  2124. player.tech_.hls.checkBuffer_();
  2125. // two failed attempts is a network error
  2126. equal(player.tech_.hls.mediaSource.error_,
  2127. 'network',
  2128. 'triggered a network error');
  2129. });
  2130. test('live stream should not call endOfStream', function(){
  2131. player.src({
  2132. src: 'https://example.com/media.m3u8',
  2133. type: 'application/vnd.apple.mpegurl'
  2134. });
  2135. openMediaSource(player);
  2136. player.tech_.trigger('play');
  2137. requests[0].respond(200, null,
  2138. '#EXTM3U\n' +
  2139. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  2140. '#EXTINF:1\n' +
  2141. '0.ts\n'
  2142. );
  2143. requests[1].response = window.bcSegment;
  2144. requests[1].respond(200, null, "");
  2145. equal("open", player.tech_.hls.mediaSource.readyState,
  2146. "media source should be in open state, not ended state for live stream after the last segment in m3u8 downloaded");
  2147. });
  2148. test('does not download segments if preload option set to none', function() {
  2149. player.preload('none');
  2150. player.src({
  2151. src: 'master.m3u8',
  2152. type: 'application/vnd.apple.mpegurl'
  2153. });
  2154. openMediaSource(player);
  2155. standardXHRResponse(requests.shift()); // master
  2156. standardXHRResponse(requests.shift()); // media
  2157. player.tech_.hls.checkBuffer_();
  2158. requests = requests.filter(function(request) {
  2159. return !/m3u8$/.test(request.uri);
  2160. });
  2161. equal(requests.length, 0, 'did not download any segments');
  2162. });
  2163. module('Buffer Inspection');
  2164. test('detects time range edges added by updates', function() {
  2165. var edges;
  2166. edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]),
  2167. videojs.createTimeRange([[0, 11]]));
  2168. deepEqual(edges, [{ end: 11 }], 'detected a forward addition');
  2169. edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]),
  2170. videojs.createTimeRange([[0, 10]]));
  2171. deepEqual(edges, [{ start: 0 }], 'detected a backward addition');
  2172. edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]),
  2173. videojs.createTimeRange([[0, 11]]));
  2174. deepEqual(edges, [
  2175. { start: 0 }, { end: 11 }
  2176. ], 'detected forward and backward additions');
  2177. edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]),
  2178. videojs.createTimeRange([[0, 10]]));
  2179. deepEqual(edges, [], 'detected no addition');
  2180. edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([]),
  2181. videojs.createTimeRange([[0, 10]]));
  2182. deepEqual(edges, [
  2183. { start: 0 },
  2184. { end: 10 }
  2185. ], 'detected an initial addition');
  2186. edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]),
  2187. videojs.createTimeRange([[0, 10], [20, 30]]));
  2188. deepEqual(edges, [
  2189. { start: 20 },
  2190. { end: 30}
  2191. ], 'detected a non-contiguous addition');
  2192. });
  2193. test('treats null buffered ranges as no addition', function() {
  2194. var edges = videojs.Hls.bufferedAdditions_(null,
  2195. videojs.createTimeRange([[0, 11]]));
  2196. equal(edges.length, 0, 'no additions');
  2197. });
  2198. })(window, window.videojs);