bbf47741 by David LaPalomento Committed by GitHub

Fmp4 support (#829)

* Media init segment support
Resolve EXT-X-MAP URI information in the playlist loader. Add support for requesting and appending initialization segments to the segment loader.

* Basic support for fragmented MP4 playback
Re-arrange source updater and track support to fit our design goals more closely. Make adjustments so that the correct source buffer types are created when a fragmented mp4 is encountered. This version will play Apple's fMp4 bipbop stream but you have to seek the player to 10 seconds after starting because the first fragment starts at 10, not 0.

* Finish consolidating audio loaders
Manage a single pair of audio playlist and segment loaders, instead of one per track. Update track logic to work with the new flow.

* Detect and set the correct starting timestamp offset
Probe the init and first MP4 segment to correctly set timestamp offset so that the stream begins at time zero. After this change, Apple's fragmented MP4 HLS example stream plays without additional modification.

* Guard against media playlists without bandwidth information
If a media playlist is loaded directly or bandwidth info is unavailable, make sure the lowest bitrate check doesn't error. Add some unnecessary request shifting to tests to avoid extraneous requests caused by the current behavior of segment loader when abort()-ing THEN pause()-ing.

* Add stub prog_index.m3u8 for tests
Some of the tests point to master playlists that reference prog_index.m3u8. Sinon caught most of the exceptions related to this but the tests weren't really exercising realistic scenarios. Add a stub prog_index to the test fixtures so that requests for prog_index don't unintentionally error.

* Abort init segment XHR alongside other segment XHRs
If the segment loader XHRs are aborted, stop the init segment one as well. Make sure to check the right property for the init segment XHR before continuing the loading process. Make sure falsey values do not cause a playlist to be blacklisted in FF for audio info changes.

* Fix audio track management after reorganization
Delay segment loader initialization steps until all starting configuration is ready. This allowed source updater MIME types to be specified early without triggering the main updater to have its audio disabled on startup. Tweak the mime type identifier function to signal alternate audio earlier. Move `this` references in segment loader's checkBuffer_ out to stateful functions to align with the original design goals. Removed a segment loader test that seemed duplicative after the checkBuffer_ change.

* Fix D3 on stats page
Update URL for D3. Remove audio switcher since it's included by default now.

* Only override codec defaults if an actual value was parsed
When converting codec strings into MIME type configurations for source buffers, make sure to use default values if the codec string didn't supply particular fields. Export the codec to MIME helper function so it can be unit-tested.

* IE fixes
Array.prototype.find() isn't available in IE so use .filter()[0] instead.

* Blacklist unsupported codecs
If MediaSource.isTypeSupported fails in a generic MP4 container, swapping to a variant with those codecs is unlikely to be successful. For instance, the fragmented bip-bop stream includes AC-3 and EC-3 audio which is not supported on Chrome or Firefox today. Exclude variants with codecs that don't pass the isTypeSupported test.
1 parent 99e84e9f
...@@ -87,6 +87,7 @@ ...@@ -87,6 +87,7 @@
87 "dependencies": { 87 "dependencies": {
88 "m3u8-parser": "^1.0.2", 88 "m3u8-parser": "^1.0.2",
89 "aes-decrypter": "^1.0.3", 89 "aes-decrypter": "^1.0.3",
90 "mux.js": "^2.4.0",
90 "video.js": "^5.10.1", 91 "video.js": "^5.10.1",
91 "videojs-contrib-media-sources": "^3.1.0", 92 "videojs-contrib-media-sources": "^3.1.0",
92 "videojs-swf": "^5.0.2", 93 "videojs-swf": "^5.0.2",
......
...@@ -97,6 +97,9 @@ const updateMaster = function(master, media) { ...@@ -97,6 +97,9 @@ const updateMaster = function(master, media) {
97 if (segment.key && !segment.key.resolvedUri) { 97 if (segment.key && !segment.key.resolvedUri) {
98 segment.key.resolvedUri = resolveUrl(playlist.resolvedUri, segment.key.uri); 98 segment.key.resolvedUri = resolveUrl(playlist.resolvedUri, segment.key.uri);
99 } 99 }
100 if (segment.map && !segment.map.resolvedUri) {
101 segment.map.resolvedUri = resolveUrl(playlist.resolvedUri, segment.map.uri);
102 }
100 } 103 }
101 changed = true; 104 changed = true;
102 } 105 }
...@@ -246,11 +249,13 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) { ...@@ -246,11 +249,13 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
246 * @return {Boolean} true if on lowest rendition 249 * @return {Boolean} true if on lowest rendition
247 */ 250 */
248 loader.isLowestEnabledRendition_ = function() { 251 loader.isLowestEnabledRendition_ = function() {
249 if (!loader.media()) { 252 let media = loader.media();
253
254 if (!media || !media.attributes) {
250 return false; 255 return false;
251 } 256 }
252 257
253 let currentPlaylist = loader.media().attributes.BANDWIDTH; 258 let currentBandwidth = loader.media().attributes.BANDWIDTH || 0;
254 259
255 return !(loader.master.playlists.filter((element, index, array) => { 260 return !(loader.master.playlists.filter((element, index, array) => {
256 let enabled = typeof element.excludeUntil === 'undefined' || 261 let enabled = typeof element.excludeUntil === 'undefined' ||
...@@ -262,7 +267,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) { ...@@ -262,7 +267,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
262 267
263 let item = element.attributes.BANDWIDTH; 268 let item = element.attributes.BANDWIDTH;
264 269
265 return item <= currentPlaylist; 270 return item <= currentBandwidth;
266 271
267 }).length > 1); 272 }).length > 1);
268 }; 273 };
...@@ -508,6 +513,12 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) { ...@@ -508,6 +513,12 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
508 // loaded a media playlist 513 // loaded a media playlist
509 // infer a master playlist if none was previously requested 514 // infer a master playlist if none was previously requested
510 loader.master = { 515 loader.master = {
516 mediaGroups: {
517 'AUDIO': {},
518 'VIDEO': {},
519 'CLOSED-CAPTIONS': {},
520 'SUBTITLES': {}
521 },
511 uri: window.location.href, 522 uri: window.location.href,
512 playlists: [{ 523 playlists: [{
513 uri: srcUrl 524 uri: srcUrl
......
...@@ -13,39 +13,12 @@ import utils from './bin-utils'; ...@@ -13,39 +13,12 @@ import utils from './bin-utils';
13 import {MediaSource, URL} from 'videojs-contrib-media-sources'; 13 import {MediaSource, URL} from 'videojs-contrib-media-sources';
14 import m3u8 from 'm3u8-parser'; 14 import m3u8 from 'm3u8-parser';
15 import videojs from 'video.js'; 15 import videojs from 'video.js';
16 import MasterPlaylistController from './master-playlist-controller'; 16 import { MasterPlaylistController } from './master-playlist-controller';
17 import Config from './config'; 17 import Config from './config';
18 import renditionSelectionMixin from './rendition-mixin'; 18 import renditionSelectionMixin from './rendition-mixin';
19 import GapSkipper from './gap-skipper'; 19 import GapSkipper from './gap-skipper';
20 import window from 'global/window'; 20 import window from 'global/window';
21 21
22 /**
23 * determine if an object a is differnt from
24 * and object b. both only having one dimensional
25 * properties
26 *
27 * @param {Object} a object one
28 * @param {Object} b object two
29 * @return {Boolean} if the object has changed or not
30 */
31 const objectChanged = function(a, b) {
32 if (typeof a !== typeof b) {
33 return true;
34 }
35 // if we have a different number of elements
36 // something has changed
37 if (Object.keys(a).length !== Object.keys(b).length) {
38 return true;
39 }
40
41 for (let prop in a) {
42 if (!b[prop] || a[prop] !== b[prop]) {
43 return true;
44 }
45 }
46 return false;
47 };
48
49 const Hls = { 22 const Hls = {
50 PlaylistLoader, 23 PlaylistLoader,
51 Playlist, 24 Playlist,
...@@ -336,7 +309,7 @@ class HlsHandler extends Component { ...@@ -336,7 +309,7 @@ class HlsHandler extends Component {
336 }); 309 });
337 310
338 this.audioTrackChange_ = () => { 311 this.audioTrackChange_ = () => {
339 this.masterPlaylistController_.useAudio(); 312 this.masterPlaylistController_.setupAudio();
340 }; 313 };
341 314
342 this.on(this.tech_, 'play', this.play); 315 this.on(this.tech_, 'play', this.play);
...@@ -436,59 +409,17 @@ class HlsHandler extends Component { ...@@ -436,59 +409,17 @@ class HlsHandler extends Component {
436 this.tech_.audioTracks().addEventListener('change', this.audioTrackChange_); 409 this.tech_.audioTracks().addEventListener('change', this.audioTrackChange_);
437 }); 410 });
438 411
439 this.masterPlaylistController_.on('audioinfo', (e) => {
440 if (!videojs.browser.IS_FIREFOX ||
441 !this.audioInfo_ ||
442 !objectChanged(this.audioInfo_, e.info)) {
443 this.audioInfo_ = e.info;
444 return;
445 }
446
447 let error = 'had different audio properties (channels, sample rate, etc.) ' +
448 'or changed in some other way. This behavior is currently ' +
449 'unsupported in Firefox due to an issue: \n\n' +
450 'https://bugzilla.mozilla.org/show_bug.cgi?id=1247138\n\n';
451
452 let enabledTrack;
453 let defaultTrack;
454
455 this.masterPlaylistController_.audioTracks_.forEach((t) => {
456 if (!defaultTrack && t.default) {
457 defaultTrack = t;
458 }
459
460 if (!enabledTrack && t.enabled) {
461 enabledTrack = t;
462 }
463 });
464
465 // they did not switch audiotracks
466 // blacklist the current playlist
467 if (!enabledTrack.getLoader(this.activeAudioGroup_())) {
468 error = `The rendition that we tried to switch to ${error}` +
469 'Unfortunately that means we will have to blacklist ' +
470 'the current playlist and switch to another. Sorry!';
471 this.masterPlaylistController_.blacklistCurrentPlaylist();
472 } else {
473 error = `The audio track '${enabledTrack.label}' that we tried to ` +
474 `switch to ${error} Unfortunately this means we will have to ` +
475 `return you to the main track '${defaultTrack.label}'. Sorry!`;
476 defaultTrack.enabled = true;
477 this.tech_.audioTracks().removeTrack(enabledTrack);
478 }
479
480 videojs.log.warn(error);
481 this.masterPlaylistController_.useAudio();
482 });
483 this.masterPlaylistController_.on('selectedinitialmedia', () => { 412 this.masterPlaylistController_.on('selectedinitialmedia', () => {
413 // Add the manual rendition mix-in to HlsHandler
414 renditionSelectionMixin(this);
415 });
416
417 this.masterPlaylistController_.on('audioupdate', () => {
484 // clear current audioTracks 418 // clear current audioTracks
485 this.tech_.clearTracks('audio'); 419 this.tech_.clearTracks('audio');
486 this.masterPlaylistController_.audioTracks_.forEach((track) => { 420 this.masterPlaylistController_.activeAudioGroup().forEach((audioTrack) => {
487 this.tech_.audioTracks().addTrack(track); 421 this.tech_.audioTracks().addTrack(audioTrack);
488 }); 422 });
489
490 // Add the manual rendition mix-in to HlsHandler
491 renditionSelectionMixin(this);
492 }); 423 });
493 424
494 // the bandwidth of the primary segment loader is our best 425 // the bandwidth of the primary segment loader is our best
......
...@@ -7,7 +7,11 @@ import { ...@@ -7,7 +7,11 @@ import {
7 standardXHRResponse, 7 standardXHRResponse,
8 openMediaSource 8 openMediaSource
9 } from './test-helpers.js'; 9 } from './test-helpers.js';
10 import MasterPlaylistController from '../src/master-playlist-controller'; 10 import manifests from './test-manifests.js';
11 import {
12 MasterPlaylistController,
13 mimeTypesForPlaylist_
14 } from '../src/master-playlist-controller';
11 /* eslint-disable no-unused-vars */ 15 /* eslint-disable no-unused-vars */
12 // we need this so that it can register hls with videojs 16 // we need this so that it can register hls with videojs
13 import { Hls } from '../src/videojs-contrib-hls'; 17 import { Hls } from '../src/videojs-contrib-hls';
...@@ -272,6 +276,37 @@ function() { ...@@ -272,6 +276,37 @@ function() {
272 '16 bytes downloaded'); 276 '16 bytes downloaded');
273 }); 277 });
274 278
279 QUnit.test('updates the enabled track when switching audio groups', function() {
280 openMediaSource(this.player, this.clock);
281 // master
282 this.requests.shift().respond(200, null,
283 manifests.multipleAudioGroupsCombinedMain);
284 // media
285 standardXHRResponse(this.requests.shift());
286 // init segment
287 standardXHRResponse(this.requests.shift());
288 // video segment
289 standardXHRResponse(this.requests.shift());
290 // audio media
291 standardXHRResponse(this.requests.shift());
292 // ignore audio segment requests
293 this.requests.length = 0;
294
295 let mpc = this.masterPlaylistController;
296 let combinedPlaylist = mpc.master().playlists[0];
297
298 mpc.masterPlaylistLoader_.media(combinedPlaylist);
299 // updated media
300 this.requests.shift().respond(200, null,
301 '#EXTM3U\n' +
302 '#EXTINF:5.0\n' +
303 '0.ts\n' +
304 '#EXT-X-ENDLIST\n');
305
306 QUnit.ok(mpc.activeAudioGroup().filter((track) => track.enabled)[0],
307 'enabled a track in the new audio group');
308 });
309
275 QUnit.test('blacklists switching from video+audio playlists to audio only', function() { 310 QUnit.test('blacklists switching from video+audio playlists to audio only', function() {
276 let audioPlaylist; 311 let audioPlaylist;
277 312
...@@ -386,6 +421,34 @@ function() { ...@@ -386,6 +421,34 @@ function() {
386 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above'); 421 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
387 }); 422 });
388 423
424 QUnit.test('blacklists the current playlist when audio changes in Firefox', function() {
425 videojs.browser.IS_FIREFOX = true;
426
427 // master
428 standardXHRResponse(this.requests.shift());
429 // media
430 standardXHRResponse(this.requests.shift());
431
432 let media = this.masterPlaylistController.media();
433
434 // initial audio config
435 this.masterPlaylistController.mediaSource.trigger({
436 type: 'audioinfo',
437 info: {}
438 });
439 // updated audio config
440
441 this.masterPlaylistController.mediaSource.trigger({
442 type: 'audioinfo',
443 info: {
444 different: true
445 }
446 });
447 QUnit.ok(media.excludeUntil > 0, 'blacklisted the old playlist');
448 QUnit.equal(this.env.log.warn.callCount, 2, 'logged two warnings');
449 this.env.log.warn.callCount = 0;
450 });
451
389 QUnit.test('updates the combined segment loader on media changes', function() { 452 QUnit.test('updates the combined segment loader on media changes', function() {
390 let updates = []; 453 let updates = [];
391 454
...@@ -702,3 +765,116 @@ QUnit.test('respects useCueTags option', function() { ...@@ -702,3 +765,116 @@ QUnit.test('respects useCueTags option', function() {
702 765
703 videojs.options.hls = origHlsOptions; 766 videojs.options.hls = origHlsOptions;
704 }); 767 });
768
769 QUnit.module('Codec to MIME Type Conversion');
770
771 QUnit.test('recognizes muxed codec configurations', function() {
772 QUnit.deepEqual(mimeTypesForPlaylist_({ mediaGroups: {} }, {}),
773 [ 'video/mp2t; codecs="avc1.4d400d, mp4a.40.2"' ],
774 'returns a default MIME type when no codecs are present');
775
776 QUnit.deepEqual(mimeTypesForPlaylist_({
777 mediaGroups: {},
778 playlists: []
779 }, {
780 attributes: {
781 CODECS: 'mp4a.40.E,avc1.deadbeef'
782 }
783 }), [
784 'video/mp2t; codecs="avc1.deadbeef, mp4a.40.E"'
785 ], 'returned the parsed muxed type');
786 });
787
788 QUnit.test('recognizes mixed codec configurations', function() {
789 QUnit.deepEqual(mimeTypesForPlaylist_({
790 mediaGroups: {
791 AUDIO: {
792 hi: {
793 en: {},
794 es: {
795 uri: 'http://example.com/alt-audio.m3u8'
796 }
797 }
798 }
799 },
800 playlists: []
801 }, {
802 attributes: {
803 AUDIO: 'hi'
804 }
805 }), [
806 'video/mp2t; codecs="avc1.4d400d, mp4a.40.2"',
807 'audio/mp2t; codecs="mp4a.40.2"'
808 ], 'returned a default muxed type with alternate audio');
809
810 QUnit.deepEqual(mimeTypesForPlaylist_({
811 mediaGroups: {
812 AUDIO: {
813 hi: {
814 eng: {},
815 es: {
816 uri: 'http://example.com/alt-audio.m3u8'
817 }
818 }
819 }
820 },
821 playlists: []
822 }, {
823 attributes: {
824 CODECS: 'mp4a.40.E,avc1.deadbeef',
825 AUDIO: 'hi'
826 }
827 }), [
828 'video/mp2t; codecs="avc1.deadbeef, mp4a.40.E"',
829 'audio/mp2t; codecs="mp4a.40.E"'
830 ], 'returned a parsed muxed type with alternate audio');
831 });
832
833 QUnit.test('recognizes unmuxed codec configurations', function() {
834 QUnit.deepEqual(mimeTypesForPlaylist_({
835 mediaGroups: {
836 AUDIO: {
837 hi: {
838 eng: {
839 uri: 'http://example.com/eng.m3u8'
840 },
841 es: {
842 uri: 'http://example.com/eng.m3u8'
843 }
844 }
845 }
846 },
847 playlists: []
848 }, {
849 attributes: {
850 AUDIO: 'hi'
851 }
852 }), [
853 'video/mp2t; codecs="avc1.4d400d"',
854 'audio/mp2t; codecs="mp4a.40.2"'
855 ], 'returned default unmuxed types');
856
857 QUnit.deepEqual(mimeTypesForPlaylist_({
858 mediaGroups: {
859 AUDIO: {
860 hi: {
861 eng: {
862 uri: 'http://example.com/alt-audio.m3u8'
863 },
864 es: {
865 uri: 'http://example.com/eng.m3u8'
866 }
867 }
868 }
869 },
870 playlists: []
871 }, {
872 attributes: {
873 CODECS: 'mp4a.40.E,avc1.deadbeef',
874 AUDIO: 'hi'
875 }
876 }), [
877 'video/mp2t; codecs="avc1.deadbeef"',
878 'audio/mp2t; codecs="mp4a.40.E"'
879 ], 'returned parsed unmuxed types');
880 });
......
...@@ -164,6 +164,21 @@ QUnit.test('playlist loader detects if we are on lowest rendition', function() { ...@@ -164,6 +164,21 @@ QUnit.test('playlist loader detects if we are on lowest rendition', function() {
164 QUnit.ok(!loader.isLowestEnabledRendition_(), 'Detected not on lowest rendition'); 164 QUnit.ok(!loader.isLowestEnabledRendition_(), 'Detected not on lowest rendition');
165 }); 165 });
166 166
167 QUnit.test('resolves media initialization segment URIs', function() {
168 let loader = new PlaylistLoader('video/fmp4.m3u8', this.fakeHls);
169
170 loader.load();
171 this.requests.shift().respond(200, null,
172 '#EXTM3U\n' +
173 '#EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"\n' +
174 '#EXTINF:10,\n' +
175 '0.ts\n' +
176 '#EXT-X-ENDLIST\n');
177
178 QUnit.equal(loader.media().segments[0].map.resolvedUri, urlTo('video/main.mp4'),
179 'resolved init segment URI');
180 });
181
167 QUnit.test('recognizes absolute URIs and requests them unmodified', function() { 182 QUnit.test('recognizes absolute URIs and requests them unmodified', function() {
168 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls); 183 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
169 184
...@@ -347,6 +362,21 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { ...@@ -347,6 +362,21 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() {
347 QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 362 QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
348 }); 363 });
349 364
365 QUnit.test('defaults missing media groups for a media playlist', function() {
366 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
367
368 loader.load();
369 this.requests.pop().respond(200, null,
370 '#EXTM3U\n' +
371 '#EXTINF:10,\n' +
372 '0.ts\n');
373
374 QUnit.ok(loader.master.mediaGroups.AUDIO, 'defaulted audio');
375 QUnit.ok(loader.master.mediaGroups.VIDEO, 'defaulted video');
376 QUnit.ok(loader.master.mediaGroups['CLOSED-CAPTIONS'], 'defaulted closed captions');
377 QUnit.ok(loader.master.mediaGroups.SUBTITLES, 'defaulted subtitles');
378 });
379
350 QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { 380 QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() {
351 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 381 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
352 382
......
...@@ -2,12 +2,14 @@ import QUnit from 'qunit'; ...@@ -2,12 +2,14 @@ import QUnit from 'qunit';
2 import SegmentLoader from '../src/segment-loader'; 2 import SegmentLoader from '../src/segment-loader';
3 import videojs from 'video.js'; 3 import videojs from 'video.js';
4 import xhrFactory from '../src/xhr'; 4 import xhrFactory from '../src/xhr';
5 import mp4probe from 'mux.js/lib/mp4/probe';
5 import Config from '../src/config'; 6 import Config from '../src/config';
6 import { 7 import {
7 playlistWithDuration, 8 playlistWithDuration,
8 useFakeEnvironment, 9 useFakeEnvironment,
9 useFakeMediaSource 10 useFakeMediaSource
10 } from './test-helpers.js'; 11 } from './test-helpers.js';
12 import sinon from 'sinon';
11 13
12 let currentTime; 14 let currentTime;
13 let mediaSource; 15 let mediaSource;
...@@ -27,6 +29,9 @@ QUnit.module('Segment Loader', { ...@@ -27,6 +29,9 @@ QUnit.module('Segment Loader', {
27 xhr: xhrFactory() 29 xhr: xhrFactory()
28 }; 30 };
29 31
32 this.timescale = sinon.stub(mp4probe, 'timescale');
33 this.startTime = sinon.stub(mp4probe, 'startTime');
34
30 currentTime = 0; 35 currentTime = 0;
31 mediaSource = new videojs.MediaSource(); 36 mediaSource = new videojs.MediaSource();
32 mediaSource.trigger('sourceopen'); 37 mediaSource.trigger('sourceopen');
...@@ -44,6 +49,8 @@ QUnit.module('Segment Loader', { ...@@ -44,6 +49,8 @@ QUnit.module('Segment Loader', {
44 afterEach() { 49 afterEach() {
45 this.env.restore(); 50 this.env.restore();
46 this.mse.restore(); 51 this.mse.restore();
52 this.timescale.restore();
53 this.startTime.restore();
47 } 54 }
48 }); 55 });
49 56
...@@ -123,7 +130,6 @@ QUnit.test('calling load should unpause', function() { ...@@ -123,7 +130,6 @@ QUnit.test('calling load should unpause', function() {
123 loader.pause(); 130 loader.pause();
124 131
125 loader.mimeType(this.mimeType); 132 loader.mimeType(this.mimeType);
126 sourceBuffer = mediaSource.sourceBuffers[0];
127 133
128 loader.load(); 134 loader.load();
129 QUnit.equal(loader.paused(), false, 'loading unpauses'); 135 QUnit.equal(loader.paused(), false, 'loading unpauses');
...@@ -138,6 +144,7 @@ QUnit.test('calling load should unpause', function() { ...@@ -138,6 +144,7 @@ QUnit.test('calling load should unpause', function() {
138 QUnit.equal(loader.paused(), false, 'unpaused during processing'); 144 QUnit.equal(loader.paused(), false, 'unpaused during processing');
139 145
140 loader.pause(); 146 loader.pause();
147 sourceBuffer = mediaSource.sourceBuffers[0];
141 sourceBuffer.trigger('updateend'); 148 sourceBuffer.trigger('updateend');
142 QUnit.equal(loader.state, 'READY', 'finished processing'); 149 QUnit.equal(loader.state, 'READY', 'finished processing');
143 QUnit.ok(loader.paused(), 'stayed paused'); 150 QUnit.ok(loader.paused(), 'stayed paused');
...@@ -236,6 +243,32 @@ QUnit.test('segment request timeouts reset bandwidth', function() { ...@@ -236,6 +243,32 @@ QUnit.test('segment request timeouts reset bandwidth', function() {
236 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time'); 243 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time');
237 }); 244 });
238 245
246 QUnit.test('updates timestamps when segments do not start at zero', function() {
247 let playlist = playlistWithDuration(10);
248
249 playlist.segments.forEach((segment) => {
250 segment.map = {
251 resolvedUri: 'init.mp4',
252 bytes: new Uint8Array(10)
253 };
254 });
255 loader.playlist(playlist);
256 loader.mimeType('video/mp4');
257 loader.load();
258
259 this.startTime.returns(11);
260
261 this.clock.tick(100);
262 // init
263 this.requests[0].response = new Uint8Array(10).buffer;
264 this.requests.shift().respond(200, null, '');
265 // segment
266 this.requests[0].response = new Uint8Array(10).buffer;
267 this.requests.shift().respond(200, null, '');
268
269 QUnit.equal(loader.sourceUpdater_.timestampOffset(), -11, 'set timestampOffset');
270 });
271
239 QUnit.test('appending a segment triggers progress', function() { 272 QUnit.test('appending a segment triggers progress', function() {
240 let progresses = 0; 273 let progresses = 0;
241 274
...@@ -482,6 +515,104 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made after ' ...@@ -482,6 +515,104 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made after '
482 QUnit.equal(this.requests.length, 0, 'no more requests are made'); 515 QUnit.equal(this.requests.length, 0, 'no more requests are made');
483 }); 516 });
484 517
518 QUnit.test('downloads init segments if specified', function() {
519 let playlist = playlistWithDuration(20);
520 let map = {
521 resolvedUri: 'main.mp4',
522 byterange: {
523 length: 20,
524 offset: 0
525 }
526 };
527
528 playlist.segments[0].map = map;
529 playlist.segments[1].map = map;
530 loader.playlist(playlist);
531 loader.mimeType(this.mimeType);
532
533 loader.load();
534 let sourceBuffer = mediaSource.sourceBuffers[0];
535
536 QUnit.equal(this.requests.length, 2, 'made requests');
537
538 // init segment response
539 this.clock.tick(1);
540 QUnit.equal(this.requests[0].url, 'main.mp4', 'requested the init segment');
541 this.requests[0].response = new Uint8Array(20).buffer;
542 this.requests.shift().respond(200, null, '');
543 // 0.ts response
544 this.clock.tick(1);
545 QUnit.equal(this.requests[0].url, '0.ts',
546 'requested the segment');
547 this.requests[0].response = new Uint8Array(20).buffer;
548 this.requests.shift().respond(200, null, '');
549
550 // append the init segment
551 sourceBuffer.buffered = videojs.createTimeRanges([]);
552 sourceBuffer.trigger('updateend');
553 // append the segment
554 sourceBuffer.buffered = videojs.createTimeRanges([[0, 10]]);
555 sourceBuffer.trigger('updateend');
556
557 QUnit.equal(this.requests.length, 1, 'made a request');
558 QUnit.equal(this.requests[0].url, '1.ts',
559 'did not re-request the init segment');
560 });
561
562 QUnit.test('detects init segment changes and downloads it', function() {
563 let playlist = playlistWithDuration(20);
564
565 playlist.segments[0].map = {
566 resolvedUri: 'init0.mp4',
567 byterange: {
568 length: 20,
569 offset: 0
570 }
571 };
572 playlist.segments[1].map = {
573 resolvedUri: 'init0.mp4',
574 byterange: {
575 length: 20,
576 offset: 20
577 }
578 };
579 loader.playlist(playlist);
580 loader.mimeType(this.mimeType);
581
582 loader.load();
583 let sourceBuffer = mediaSource.sourceBuffers[0];
584
585 QUnit.equal(this.requests.length, 2, 'made requests');
586
587 // init segment response
588 this.clock.tick(1);
589 QUnit.equal(this.requests[0].url, 'init0.mp4', 'requested the init segment');
590 QUnit.equal(this.requests[0].headers.Range, 'bytes=0-19',
591 'requested the init segment byte range');
592 this.requests[0].response = new Uint8Array(20).buffer;
593 this.requests.shift().respond(200, null, '');
594 // 0.ts response
595 this.clock.tick(1);
596 QUnit.equal(this.requests[0].url, '0.ts',
597 'requested the segment');
598 this.requests[0].response = new Uint8Array(20).buffer;
599 this.requests.shift().respond(200, null, '');
600
601 // append the init segment
602 sourceBuffer.buffered = videojs.createTimeRanges([]);
603 sourceBuffer.trigger('updateend');
604 // append the segment
605 sourceBuffer.buffered = videojs.createTimeRanges([[0, 10]]);
606 sourceBuffer.trigger('updateend');
607
608 QUnit.equal(this.requests.length, 2, 'made requests');
609 QUnit.equal(this.requests[0].url, 'init0.mp4', 'requested the init segment');
610 QUnit.equal(this.requests[0].headers.Range, 'bytes=20-39',
611 'requested the init segment byte range');
612 QUnit.equal(this.requests[1].url, '1.ts',
613 'did not re-request the init segment');
614 });
615
485 QUnit.test('cancels outstanding requests on abort', function() { 616 QUnit.test('cancels outstanding requests on abort', function() {
486 loader.playlist(playlistWithDuration(20)); 617 loader.playlist(playlistWithDuration(20));
487 loader.mimeType(this.mimeType); 618 loader.mimeType(this.mimeType);
...@@ -899,16 +1030,21 @@ QUnit.test('key request timeouts reset bandwidth', function() { ...@@ -899,16 +1030,21 @@ QUnit.test('key request timeouts reset bandwidth', function() {
899 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time'); 1030 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time');
900 }); 1031 });
901 1032
902 QUnit.test('GOAL_BUFFER_LENGTH changes to 1 segment ' + 1033 QUnit.test('checks the goal buffer configuration every loading opportunity', function() {
903 ' which is already buffered, no new request is formed', function() { 1034 let playlist = playlistWithDuration(20);
1035 let defaultGoal = Config.GOAL_BUFFER_LENGTH;
1036 let segmentInfo;
1037
904 Config.GOAL_BUFFER_LENGTH = 1; 1038 Config.GOAL_BUFFER_LENGTH = 1;
1039 loader.playlist(playlist);
905 loader.mimeType(this.mimeType); 1040 loader.mimeType(this.mimeType);
906 let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]), 1041 loader.load();
907 playlistWithDuration(20),
908 0);
909 1042
1043 segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]),
1044 playlist,
1045 0);
910 QUnit.ok(!segmentInfo, 'no request generated'); 1046 QUnit.ok(!segmentInfo, 'no request generated');
911 Config.GOAL_BUFFER_LENGTH = 30; 1047 Config.GOAL_BUFFER_LENGTH = defaultGoal;
912 }); 1048 });
913 1049
914 QUnit.module('Segment Loading Calculation', { 1050 QUnit.module('Segment Loading Calculation', {
...@@ -1031,22 +1167,6 @@ function() { ...@@ -1031,22 +1167,6 @@ function() {
1031 QUnit.ok(!segmentInfo, 'no request was made'); 1167 QUnit.ok(!segmentInfo, 'no request was made');
1032 }); 1168 });
1033 1169
1034 QUnit.test('calculates timestampOffset for discontinuities', function() {
1035 let segmentInfo;
1036 let playlist;
1037
1038 loader.mimeType(this.mimeType);
1039
1040 playlist = playlistWithDuration(60);
1041 playlist.segments[3].end = 37.9;
1042 playlist.discontinuityStarts = [4];
1043 playlist.segments[4].discontinuity = true;
1044 playlist.segments[4].timeline = 1;
1045
1046 segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 37.9]]), playlist, 36);
1047 QUnit.equal(segmentInfo.timestampOffset, 37.9, 'placed the discontinuous segment');
1048 });
1049
1050 QUnit.test('adjusts calculations based on expired time', function() { 1170 QUnit.test('adjusts calculations based on expired time', function() {
1051 let buffered; 1171 let buffered;
1052 let playlist; 1172 let playlist;
......
...@@ -118,7 +118,13 @@ let fakeEnvironment = { ...@@ -118,7 +118,13 @@ let fakeEnvironment = {
118 ['warn', 'error'].forEach((level) => { 118 ['warn', 'error'].forEach((level) => {
119 if (this.log && this.log[level] && this.log[level].restore) { 119 if (this.log && this.log[level] && this.log[level].restore) {
120 if (QUnit) { 120 if (QUnit) {
121 QUnit.equal(this.log[level].callCount, 0, `no unexpected logs on ${level}`); 121 let calls = this.log[level].args.map((args) => {
122 return args.join(', ');
123 }).join('\n ');
124
125 QUnit.equal(this.log[level].callCount,
126 0,
127 'no unexpected logs at level "' + level + '":\n ' + calls);
122 } 128 }
123 this.log[level].restore(); 129 this.log[level].restore();
124 } 130 }
......
1 #EXTM3U
2 #EXT-X-TARGETDURATION:6
3 #EXT-X-VERSION:7
4 #EXT-X-MEDIA-SEQUENCE:1
5 #EXT-X-PLAYLIST-TYPE:VOD
6 #EXT-X-INDEPENDENT-SEGMENTS
7 #EXT-X-MAP:URI="main.mp4",BYTERANGE="604@0"
8 #EXTINF:5.99467,
9 #EXT-X-BYTERANGE:118151@604
10 main.mp4
11 #EXTINF:5.99467,
12 #EXT-X-BYTERANGE:119253@118755
13 main.mp4
14 #EXTINF:5.99467,
15 #EXT-X-BYTERANGE:119258@238008
16 main.mp4
17 #EXTINF:5.99467,
18 #EXT-X-BYTERANGE:119253@357266
19 main.mp4
20 #EXTINF:5.99467,
21 #EXT-X-BYTERANGE:119255@476519
22 main.mp4
23 #EXTINF:5.99467,
24 #EXT-X-BYTERANGE:119253@595774
25 main.mp4
26 #EXTINF:5.99467,
27 #EXT-X-BYTERANGE:119258@715027
28 main.mp4
29 #EXTINF:5.99467,
30 #EXT-X-BYTERANGE:119253@834285
31 main.mp4
32 #EXTINF:5.99467,
33 #EXT-X-BYTERANGE:119258@953538
34 main.mp4
35 #EXTINF:5.99467,
36 #EXT-X-BYTERANGE:119254@1072796
37 main.mp4
38 #EXTINF:5.99467,
39 #EXT-X-BYTERANGE:119254@1192050
40 main.mp4
41 #EXTINF:5.99467,
42 #EXT-X-BYTERANGE:119257@1311304
43 main.mp4
44 #EXTINF:5.99467,
45 #EXT-X-BYTERANGE:119258@1430561
46 main.mp4
47 #EXTINF:5.99467,
48 #EXT-X-BYTERANGE:119258@1549819
49 main.mp4
50 #EXTINF:5.99467,
51 #EXT-X-BYTERANGE:119254@1669077
52 main.mp4
53 #EXTINF:5.99467,
54 #EXT-X-BYTERANGE:119257@1788331
55 main.mp4
56 #EXTINF:5.99467,
57 #EXT-X-BYTERANGE:119258@1907588
58 main.mp4
59 #EXTINF:5.99467,
60 #EXT-X-BYTERANGE:119259@2026846
61 main.mp4
62 #EXTINF:5.99467,
63 #EXT-X-BYTERANGE:119257@2146105
64 main.mp4
65 #EXTINF:5.99467,
66 #EXT-X-BYTERANGE:119254@2265362
67 main.mp4
68 #EXTINF:5.99467,
69 #EXT-X-BYTERANGE:119258@2384616
70 main.mp4
71 #EXTINF:5.99467,
72 #EXT-X-BYTERANGE:119258@2503874
73 main.mp4
74 #EXTINF:5.99467,
75 #EXT-X-BYTERANGE:119257@2623132
76 main.mp4
77 #EXTINF:5.99467,
78 #EXT-X-BYTERANGE:119254@2742389
79 main.mp4
80 #EXTINF:5.99467,
81 #EXT-X-BYTERANGE:119253@2861643
82 main.mp4
83 #EXTINF:5.99467,
84 #EXT-X-BYTERANGE:119258@2980896
85 main.mp4
86 #EXTINF:5.99467,
87 #EXT-X-BYTERANGE:119254@3100154
88 main.mp4
89 #EXTINF:5.99467,
90 #EXT-X-BYTERANGE:119254@3219408
91 main.mp4
92 #EXTINF:5.99467,
93 #EXT-X-BYTERANGE:119258@3338662
94 main.mp4
95 #EXTINF:5.99467,
96 #EXT-X-BYTERANGE:119253@3457920
97 main.mp4
98 #EXTINF:5.99467,
99 #EXT-X-BYTERANGE:119258@3577173
100 main.mp4
101 #EXTINF:5.99467,
102 #EXT-X-BYTERANGE:119253@3696431
103 main.mp4
104 #EXTINF:5.99467,
105 #EXT-X-BYTERANGE:119258@3815684
106 main.mp4
107 #EXTINF:5.99467,
108 #EXT-X-BYTERANGE:119258@3934942
109 main.mp4
110 #EXTINF:5.99467,
111 #EXT-X-BYTERANGE:119254@4054200
112 main.mp4
113 #EXTINF:5.99467,
114 #EXT-X-BYTERANGE:119254@4173454
115 main.mp4
116 #EXTINF:5.99467,
117 #EXT-X-BYTERANGE:119253@4292708
118 main.mp4
119 #EXTINF:5.99467,
120 #EXT-X-BYTERANGE:119255@4411961
121 main.mp4
122 #EXTINF:5.99467,
123 #EXT-X-BYTERANGE:119257@4531216
124 main.mp4
125 #EXTINF:5.99467,
126 #EXT-X-BYTERANGE:119254@4650473
127 main.mp4
128 #EXTINF:5.99467,
129 #EXT-X-BYTERANGE:119257@4769727
130 main.mp4
131 #EXTINF:5.99467,
132 #EXT-X-BYTERANGE:119259@4888984
133 main.mp4
134 #EXTINF:5.99467,
135 #EXT-X-BYTERANGE:119257@5008243
136 main.mp4
137 #EXTINF:5.99467,
138 #EXT-X-BYTERANGE:119257@5127500
139 main.mp4
140 #EXTINF:5.99467,
141 #EXT-X-BYTERANGE:119259@5246757
142 main.mp4
143 #EXTINF:5.99467,
144 #EXT-X-BYTERANGE:119253@5366016
145 main.mp4
146 #EXTINF:5.99467,
147 #EXT-X-BYTERANGE:119258@5485269
148 main.mp4
149 #EXTINF:5.99467,
150 #EXT-X-BYTERANGE:119258@5604527
151 main.mp4
152 #EXTINF:5.99467,
153 #EXT-X-BYTERANGE:119253@5723785
154 main.mp4
155 #EXTINF:5.99467,
156 #EXT-X-BYTERANGE:119255@5843038
157 main.mp4
158 #EXTINF:5.99467,
159 #EXT-X-BYTERANGE:119257@5962293
160 main.mp4
161 #EXTINF:5.99467,
162 #EXT-X-BYTERANGE:119257@6081550
163 main.mp4
164 #EXTINF:5.99467,
165 #EXT-X-BYTERANGE:119258@6200807
166 main.mp4
167 #EXTINF:5.99467,
168 #EXT-X-BYTERANGE:119259@6320065
169 main.mp4
170 #EXTINF:5.99467,
171 #EXT-X-BYTERANGE:119257@6439324
172 main.mp4
173 #EXTINF:5.99467,
174 #EXT-X-BYTERANGE:119254@6558581
175 main.mp4
176 #EXTINF:5.99467,
177 #EXT-X-BYTERANGE:119258@6677835
178 main.mp4
179 #EXTINF:5.99467,
180 #EXT-X-BYTERANGE:119257@6797093
181 main.mp4
182 #EXTINF:5.99467,
183 #EXT-X-BYTERANGE:119254@6916350
184 main.mp4
185 #EXTINF:5.99467,
186 #EXT-X-BYTERANGE:119257@7035604
187 main.mp4
188 #EXTINF:5.99467,
189 #EXT-X-BYTERANGE:119255@7154861
190 main.mp4
191 #EXTINF:5.99467,
192 #EXT-X-BYTERANGE:119253@7274116
193 main.mp4
194 #EXTINF:5.99467,
195 #EXT-X-BYTERANGE:119254@7393369
196 main.mp4
197 #EXTINF:5.99467,
198 #EXT-X-BYTERANGE:119254@7512623
199 main.mp4
200 #EXTINF:5.99467,
201 #EXT-X-BYTERANGE:119253@7631877
202 main.mp4
203 #EXTINF:5.99467,
204 #EXT-X-BYTERANGE:119258@7751130
205 main.mp4
206 #EXTINF:5.99467,
207 #EXT-X-BYTERANGE:119258@7870388
208 main.mp4
209 #EXTINF:5.99467,
210 #EXT-X-BYTERANGE:119258@7989646
211 main.mp4
212 #EXTINF:5.99467,
213 #EXT-X-BYTERANGE:119253@8108904
214 main.mp4
215 #EXTINF:5.99467,
216 #EXT-X-BYTERANGE:119258@8228157
217 main.mp4
218 #EXTINF:5.99467,
219 #EXT-X-BYTERANGE:119258@8347415
220 main.mp4
221 #EXTINF:5.99467,
222 #EXT-X-BYTERANGE:119253@8466673
223 main.mp4
224 #EXTINF:5.99467,
225 #EXT-X-BYTERANGE:119259@8585926
226 main.mp4
227 #EXTINF:5.99467,
228 #EXT-X-BYTERANGE:119257@8705185
229 main.mp4
230 #EXTINF:5.99467,
231 #EXT-X-BYTERANGE:119254@8824442
232 main.mp4
233 #EXTINF:5.99467,
234 #EXT-X-BYTERANGE:119258@8943696
235 main.mp4
236 #EXTINF:5.99467,
237 #EXT-X-BYTERANGE:119253@9062954
238 main.mp4
239 #EXTINF:5.99467,
240 #EXT-X-BYTERANGE:119259@9182207
241 main.mp4
242 #EXTINF:5.99467,
243 #EXT-X-BYTERANGE:119257@9301466
244 main.mp4
245 #EXTINF:5.99467,
246 #EXT-X-BYTERANGE:119258@9420723
247 main.mp4
248 #EXTINF:5.99467,
249 #EXT-X-BYTERANGE:119389@9539981
250 main.mp4
251 #EXTINF:5.99467,
252 #EXT-X-BYTERANGE:119265@9659370
253 main.mp4
254 #EXTINF:5.99467,
255 #EXT-X-BYTERANGE:119533@9778635
256 main.mp4
257 #EXTINF:5.99467,
258 #EXT-X-BYTERANGE:119868@9898168
259 main.mp4
260 #EXTINF:5.99467,
261 #EXT-X-BYTERANGE:119140@10018036
262 main.mp4
263 #EXTINF:5.99467,
264 #EXT-X-BYTERANGE:118985@10137176
265 main.mp4
266 #EXTINF:5.99467,
267 #EXT-X-BYTERANGE:118701@10256161
268 main.mp4
269 #EXTINF:5.99467,
270 #EXT-X-BYTERANGE:119180@10374862
271 main.mp4
272 #EXTINF:5.99467,
273 #EXT-X-BYTERANGE:119259@10494042
274 main.mp4
275 #EXTINF:5.99467,
276 #EXT-X-BYTERANGE:119257@10613301
277 main.mp4
278 #EXTINF:5.99467,
279 #EXT-X-BYTERANGE:119254@10732558
280 main.mp4
281 #EXTINF:5.99467,
282 #EXT-X-BYTERANGE:119257@10851812
283 main.mp4
284 #EXTINF:5.99467,
285 #EXT-X-BYTERANGE:119258@10971069
286 main.mp4
287 #EXTINF:5.99467,
288 #EXT-X-BYTERANGE:119258@11090327
289 main.mp4
290 #EXTINF:5.99467,
291 #EXT-X-BYTERANGE:119258@11209585
292 main.mp4
293 #EXTINF:5.99467,
294 #EXT-X-BYTERANGE:119258@11328843
295 main.mp4
296 #EXTINF:5.99467,
297 #EXT-X-BYTERANGE:119258@11448101
298 main.mp4
299 #EXTINF:5.99467,
300 #EXT-X-BYTERANGE:119258@11567359
301 main.mp4
302 #EXTINF:5.99467,
303 #EXT-X-BYTERANGE:119257@11686617
304 main.mp4
305 #EXTINF:5.99467,
306 #EXT-X-BYTERANGE:119254@11805874
307 main.mp4
308 #EXTINF:1.13067,
309 #EXT-X-BYTERANGE:22563@11925128
310 main.mp4
311 #EXT-X-ENDLIST
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
14 14
15 <!-- player stats visualization --> 15 <!-- player stats visualization -->
16 <link href="stats.css" rel="stylesheet"> 16 <link href="stats.css" rel="stylesheet">
17 <script src="../switcher/js/vendor/d3.min.js"></script> 17 <script src="/node_modules/d3/d3.min.js"></script>
18 18
19 <style> 19 <style>
20 body { 20 body {
...@@ -212,12 +212,6 @@ ...@@ -212,12 +212,6 @@
212 player.ready(function() { 212 player.ready(function() {
213 213
214 // ------------ 214 // ------------
215 // Audio Track Switcher
216 // ------------
217
218 player.controlBar.addChild('AudioTrackButton', {}, 13);
219
220 // ------------
221 // Player Stats 215 // Player Stats
222 // ------------ 216 // ------------
223 217
......