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",
......
...@@ -5,35 +5,159 @@ import PlaylistLoader from './playlist-loader'; ...@@ -5,35 +5,159 @@ import PlaylistLoader from './playlist-loader';
5 import SegmentLoader from './segment-loader'; 5 import SegmentLoader from './segment-loader';
6 import Ranges from './ranges'; 6 import Ranges from './ranges';
7 import videojs from 'video.js'; 7 import videojs from 'video.js';
8 import HlsAudioTrack from './hls-audio-track';
9 import AdCueTags from './ad-cue-tags'; 8 import AdCueTags from './ad-cue-tags';
10 9
11 // 5 minute blacklist 10 // 5 minute blacklist
12 const BLACKLIST_DURATION = 5 * 60 * 1000; 11 const BLACKLIST_DURATION = 5 * 60 * 1000;
13 let Hls; 12 let Hls;
14 13
14 /**
15 * determine if an object a is differnt from
16 * and object b. both only having one dimensional
17 * properties
18 *
19 * @param {Object} a object one
20 * @param {Object} b object two
21 * @return {Boolean} if the object has changed or not
22 */
23 const objectChanged = function(a, b) {
24 if (typeof a !== typeof b) {
25 return true;
26 }
27 // if we have a different number of elements
28 // something has changed
29 if (Object.keys(a).length !== Object.keys(b).length) {
30 return true;
31 }
32
33 for (let prop in a) {
34 if (a[prop] !== b[prop]) {
35 return true;
36 }
37 }
38 return false;
39 };
40
41 /**
42 * Parses a codec string to retrieve the number of codecs specified,
43 * the video codec and object type indicator, and the audio profile.
44 *
45 * @private
46 */
15 const parseCodecs = function(codecs) { 47 const parseCodecs = function(codecs) {
16 let result = { 48 let result = {
17 codecCount: 0, 49 codecCount: 0,
18 videoCodec: null, 50 videoCodec: null,
51 videoObjectTypeIndicator: null,
19 audioProfile: null 52 audioProfile: null
20 }; 53 };
54 let parsed;
21 55
22 result.codecCount = codecs.split(',').length; 56 result.codecCount = codecs.split(',').length;
23 result.codecCount = result.codecCount || 2; 57 result.codecCount = result.codecCount || 2;
24 58
25 // parse the video codec but ignore the version 59 // parse the video codec
26 result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs); 60 parsed = (/(^|\s|,)+(avc1)([^ ,]*)/i).exec(codecs);
27 result.videoCodec = result.videoCodec && result.videoCodec[2]; 61 if (parsed) {
62 result.videoCodec = parsed[2];
63 result.videoObjectTypeIndicator = parsed[3];
64 }
28 65
29 // parse the last field of the audio codec 66 // parse the last field of the audio codec
30 result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs); 67 result.audioProfile =
68 (/(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i).exec(codecs);
31 result.audioProfile = result.audioProfile && result.audioProfile[2]; 69 result.audioProfile = result.audioProfile && result.audioProfile[2];
32 70
33 return result; 71 return result;
34 }; 72 };
35 73
36 /** 74 /**
75 * Calculates the MIME type strings for a working configuration of
76 * SourceBuffers to play variant streams in a master playlist. If
77 * there is no possible working configuration, an empty array will be
78 * returned.
79 *
80 * @param master {Object} the m3u8 object for the master playlist
81 * @param media {Object} the m3u8 object for the variant playlist
82 * @return {Array} the MIME type strings. If the array has more than
83 * one entry, the first element should be applied to the video
84 * SourceBuffer and the second to the audio SourceBuffer.
85 *
86 * @private
87 */
88 export const mimeTypesForPlaylist_ = function(master, media) {
89 let container = 'mp2t';
90 let codecs = {
91 videoCodec: 'avc1',
92 videoObjectTypeIndicator: '.4d400d',
93 audioProfile: '2'
94 };
95 let audioGroup = [];
96 let mediaAttributes;
97 let previousGroup = null;
98
99 if (!media) {
100 // not enough information, return an error
101 return [];
102 }
103 // An initialization segment means the media playlists is an iframe
104 // playlist or is using the mp4 container. We don't currently
105 // support iframe playlists, so assume this is signalling mp4
106 // fragments.
107 // the existence check for segments can be removed once
108 // https://github.com/videojs/m3u8-parser/issues/8 is closed
109 if (media.segments && media.segments.length && media.segments[0].map) {
110 container = 'mp4';
111 }
112
113 // if the codecs were explicitly specified, use them instead of the
114 // defaults
115 mediaAttributes = media.attributes || {};
116 if (mediaAttributes.CODECS) {
117 let parsedCodecs = parseCodecs(mediaAttributes.CODECS);
118
119 Object.keys(parsedCodecs).forEach((key) => {
120 codecs[key] = parsedCodecs[key] || codecs[key];
121 });
122 }
123
124 if (master.mediaGroups.AUDIO) {
125 audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO];
126 }
127
128 // if audio could be muxed or unmuxed, use mime types appropriate
129 // for both scenarios
130 for (let groupId in audioGroup) {
131 if (previousGroup && (!!audioGroup[groupId].uri !== !!previousGroup.uri)) {
132 // one source buffer with muxed video and audio and another for
133 // the alternate audio
134 return [
135 'video/' + container + '; codecs="' +
136 codecs.videoCodec + codecs.videoObjectTypeIndicator + ', mp4a.40.' + codecs.audioProfile + '"',
137 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"'
138 ];
139 }
140 previousGroup = audioGroup[groupId];
141 }
142 // if all video and audio is unmuxed, use two single-codec mime
143 // types
144 if (previousGroup && previousGroup.uri) {
145 return [
146 'video/' + container + '; codecs="' +
147 codecs.videoCodec + codecs.videoObjectTypeIndicator + '"',
148 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"'
149 ];
150 }
151
152 // all video and audio are muxed, use a dual-codec mime type
153 return [
154 'video/' + container + '; codecs="' +
155 codecs.videoCodec + codecs.videoObjectTypeIndicator +
156 ', mp4a.40.' + codecs.audioProfile + '"'
157 ];
158 };
159
160 /**
37 * the master playlist controller controller all interactons 161 * the master playlist controller controller all interactons
38 * between playlists and segmentloaders. At this time this mainly 162 * between playlists and segmentloaders. At this time this mainly
39 * involves a master playlist and a series of audio playlists 163 * involves a master playlist and a series of audio playlists
...@@ -42,8 +166,11 @@ const parseCodecs = function(codecs) { ...@@ -42,8 +166,11 @@ const parseCodecs = function(codecs) {
42 * @class MasterPlaylistController 166 * @class MasterPlaylistController
43 * @extends videojs.EventTarget 167 * @extends videojs.EventTarget
44 */ 168 */
45 export default class MasterPlaylistController extends videojs.EventTarget { 169 export class MasterPlaylistController extends videojs.EventTarget {
46 constructor({ 170 constructor(options) {
171 super();
172
173 let {
47 url, 174 url,
48 withCredentials, 175 withCredentials,
49 mode, 176 mode,
...@@ -51,8 +178,11 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -51,8 +178,11 @@ export default class MasterPlaylistController extends videojs.EventTarget {
51 bandwidth, 178 bandwidth,
52 externHls, 179 externHls,
53 useCueTags 180 useCueTags
54 }) { 181 } = options;
55 super(); 182
183 if (!url) {
184 throw new Error('A non-empty playlist URL is required');
185 }
56 186
57 Hls = externHls; 187 Hls = externHls;
58 188
...@@ -74,8 +204,12 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -74,8 +204,12 @@ export default class MasterPlaylistController extends videojs.EventTarget {
74 timeout: null 204 timeout: null
75 }; 205 };
76 206
207 this.audioGroups_ = {};
208
77 this.mediaSource = new videojs.MediaSource({ mode }); 209 this.mediaSource = new videojs.MediaSource({ mode });
78 this.mediaSource.on('audioinfo', (e) => this.trigger(e)); 210 this.audioinfo_ = null;
211 this.mediaSource.on('audioinfo', this.handleAudioinfoUpdate_.bind(this));
212
79 // load the media source into the player 213 // load the media source into the player
80 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this)); 214 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
81 215
...@@ -90,17 +224,28 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -90,17 +224,28 @@ export default class MasterPlaylistController extends videojs.EventTarget {
90 bandwidth 224 bandwidth
91 }; 225 };
92 226
227 // setup playlist loaders
228 this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials);
229 this.setupMasterPlaylistLoaderListeners_();
230 this.audioPlaylistLoader_ = null;
231
232 // setup segment loaders
93 // combined audio/video or just video when alternate audio track is selected 233 // combined audio/video or just video when alternate audio track is selected
94 this.mainSegmentLoader_ = new SegmentLoader(segmentLoaderOptions); 234 this.mainSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
95 // alternate audio track 235 // alternate audio track
96 this.audioSegmentLoader_ = new SegmentLoader(segmentLoaderOptions); 236 this.audioSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
237 this.setupSegmentLoaderListeners_();
97 238
98 if (!url) { 239 this.masterPlaylistLoader_.start();
99 throw new Error('A non-empty playlist URL is required');
100 } 240 }
101 241
102 this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials); 242 /**
103 243 * Register event handlers on the master playlist loader. A helper
244 * function for construction time.
245 *
246 * @private
247 */
248 setupMasterPlaylistLoaderListeners_() {
104 this.masterPlaylistLoader_.on('loadedmetadata', () => { 249 this.masterPlaylistLoader_.on('loadedmetadata', () => {
105 let media = this.masterPlaylistLoader_.media(); 250 let media = this.masterPlaylistLoader_.media();
106 let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000; 251 let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000;
...@@ -115,9 +260,19 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -115,9 +260,19 @@ export default class MasterPlaylistController extends videojs.EventTarget {
115 this.mainSegmentLoader_.load(); 260 this.mainSegmentLoader_.load();
116 } 261 }
117 262
118 this.setupSourceBuffer_(); 263 try {
264 this.setupSourceBuffers_();
265 } catch (e) {
266 videojs.log.warn('Failed to create SourceBuffers', e);
267 return this.mediaSource.endOfStream('decode');
268 }
119 this.setupFirstPlay(); 269 this.setupFirstPlay();
120 this.useAudio(); 270
271 this.fillAudioTracks_();
272 this.setupAudio();
273 this.trigger('audioupdate');
274
275 this.trigger('selectedinitialmedia');
121 }); 276 });
122 277
123 this.masterPlaylistLoader_.on('loadedplaylist', () => { 278 this.masterPlaylistLoader_.on('loadedplaylist', () => {
...@@ -128,9 +283,6 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -128,9 +283,6 @@ export default class MasterPlaylistController extends videojs.EventTarget {
128 // select the initial variant 283 // select the initial variant
129 this.initialMedia_ = this.selectPlaylist(); 284 this.initialMedia_ = this.selectPlaylist();
130 this.masterPlaylistLoader_.media(this.initialMedia_); 285 this.masterPlaylistLoader_.media(this.initialMedia_);
131 this.fillAudioTracks_();
132
133 this.trigger('selectedinitialmedia');
134 return; 286 return;
135 } 287 }
136 288
...@@ -166,6 +318,8 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -166,6 +318,8 @@ export default class MasterPlaylistController extends videojs.EventTarget {
166 this.masterPlaylistLoader_.on('mediachange', () => { 318 this.masterPlaylistLoader_.on('mediachange', () => {
167 let media = this.masterPlaylistLoader_.media(); 319 let media = this.masterPlaylistLoader_.media();
168 let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000; 320 let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000;
321 let activeAudioGroup;
322 let activeTrack;
169 323
170 // If we don't have any more available playlists, we don't want to 324 // If we don't have any more available playlists, we don't want to
171 // timeout the request. 325 // timeout the request.
...@@ -183,12 +337,29 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -183,12 +337,29 @@ export default class MasterPlaylistController extends videojs.EventTarget {
183 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_); 337 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_);
184 this.mainSegmentLoader_.load(); 338 this.mainSegmentLoader_.load();
185 339
340 // if the audio group has changed, a new audio track has to be
341 // enabled
342 activeAudioGroup = this.activeAudioGroup();
343 activeTrack = activeAudioGroup.filter((track) => track.enabled)[0];
344 if (!activeTrack) {
345 this.setupAudio();
346 this.trigger('audioupdate');
347 }
348
186 this.tech_.trigger({ 349 this.tech_.trigger({
187 type: 'mediachange', 350 type: 'mediachange',
188 bubbles: true 351 bubbles: true
189 }); 352 });
190 }); 353 });
354 }
191 355
356 /**
357 * Register event handlers on the segment loaders. A helper function
358 * for construction time.
359 *
360 * @private
361 */
362 setupSegmentLoaderListeners_() {
192 this.mainSegmentLoader_.on('progress', () => { 363 this.mainSegmentLoader_.on('progress', () => {
193 // figure out what stream the next segment should be downloaded from 364 // figure out what stream the next segment should be downloaded from
194 // with the updated bandwidth information 365 // with the updated bandwidth information
...@@ -206,10 +377,50 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -206,10 +377,50 @@ export default class MasterPlaylistController extends videojs.EventTarget {
206 '. Switching back to default.'); 377 '. Switching back to default.');
207 this.audioSegmentLoader_.abort(); 378 this.audioSegmentLoader_.abort();
208 this.audioPlaylistLoader_ = null; 379 this.audioPlaylistLoader_ = null;
209 this.useAudio(); 380 this.setupAudio();
210 }); 381 });
382 }
211 383
212 this.masterPlaylistLoader_.load(); 384 handleAudioinfoUpdate_(event) {
385 if (!videojs.browser.IS_FIREFOX ||
386 !this.audioInfo_ ||
387 !objectChanged(this.audioInfo_, event.info)) {
388 this.audioInfo_ = event.info;
389 return;
390 }
391
392 let error = 'had different audio properties (channels, sample rate, etc.) ' +
393 'or changed in some other way. This behavior is currently ' +
394 'unsupported in Firefox due to an issue: \n\n' +
395 'https://bugzilla.mozilla.org/show_bug.cgi?id=1247138\n\n';
396
397 let enabledIndex =
398 this.activeAudioGroup()
399 .map((track) => track.enabled)
400 .indexOf(true);
401 let enabledTrack = this.activeAudioGroup()[enabledIndex];
402 let defaultTrack = this.activeAudioGroup().filter((track) => {
403 return track.properties_ && track.properties_.default;
404 })[0];
405
406 // they did not switch audiotracks
407 // blacklist the current playlist
408 if (!this.audioPlaylistLoader_) {
409 error = `The rendition that we tried to switch to ${error}` +
410 'Unfortunately that means we will have to blacklist ' +
411 'the current playlist and switch to another. Sorry!';
412 this.blacklistCurrentPlaylist();
413 } else {
414 error = `The audio track '${enabledTrack.label}' that we tried to ` +
415 `switch to ${error} Unfortunately this means we will have to ` +
416 `return you to the main track '${defaultTrack.label}'. Sorry!`;
417 defaultTrack.enabled = true;
418 this.activeAudioGroup().splice(enabledIndex, 1);
419 this.trigger('audioupdate');
420 }
421
422 videojs.log.warn(error);
423 this.setupAudio();
213 } 424 }
214 425
215 /** 426 /**
...@@ -264,34 +475,33 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -264,34 +475,33 @@ export default class MasterPlaylistController extends videojs.EventTarget {
264 Object.keys(mediaGroups.AUDIO).length === 0 || 475 Object.keys(mediaGroups.AUDIO).length === 0 ||
265 this.mode_ !== 'html5') { 476 this.mode_ !== 'html5') {
266 // "main" audio group, track name "default" 477 // "main" audio group, track name "default"
267 mediaGroups.AUDIO = {main: {default: {default: true }}}; 478 mediaGroups.AUDIO = { main: { default: { default: true }}};
268 } 479 }
269 480
270 let tracks = {};
271
272 for (let mediaGroup in mediaGroups.AUDIO) { 481 for (let mediaGroup in mediaGroups.AUDIO) {
273 for (let label in mediaGroups.AUDIO[mediaGroup]) { 482 if (!this.audioGroups_[mediaGroup]) {
274 let properties = mediaGroups.AUDIO[mediaGroup][label]; 483 this.audioGroups_[mediaGroup] = [];
275
276 // if the track already exists add a new "location"
277 // since tracks in different mediaGroups are actually the same
278 // track with different locations to download them from
279 if (tracks[label]) {
280 tracks[label].addLoader(mediaGroup, properties.resolvedUri);
281 continue;
282 } 484 }
283 485
284 let track = new HlsAudioTrack(videojs.mergeOptions(properties, { 486 for (let label in mediaGroups.AUDIO[mediaGroup]) {
285 hls: this.hls_, 487 let properties = mediaGroups.AUDIO[mediaGroup][label];
286 withCredentials: this.withCredential, 488 let track = new videojs.AudioTrack({
287 mediaGroup, 489 id: label,
490 kind: properties.default ? 'main' : 'alternative',
491 enabled: false,
492 language: properties.language,
288 label 493 label
289 })); 494 });
290 495
291 tracks[label] = track; 496 track.properties_ = properties;
292 this.audioTracks_.push(track); 497 this.audioGroups_[mediaGroup].push(track);
293 } 498 }
294 } 499 }
500
501 // enable the default active track
502 (this.activeAudioGroup().filter((audioTrack) => {
503 return audioTrack.properties_.default;
504 })[0] || this.activeAudioGroup()[0]).enabled = true;
295 } 505 }
296 506
297 /** 507 /**
...@@ -305,81 +515,74 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -305,81 +515,74 @@ export default class MasterPlaylistController extends videojs.EventTarget {
305 } 515 }
306 516
307 /** 517 /**
308 * Get the current active Media Group for Audio 518 * Returns the audio group for the currently active primary
309 * given the selected playlist and its attributes 519 * media playlist.
310 */ 520 */
311 activeAudioGroup() { 521 activeAudioGroup() {
312 let media = this.masterPlaylistLoader_.media(); 522 let videoPlaylist = this.masterPlaylistLoader_.media();
313 let mediaGroup = 'main'; 523 let result;
314 524
315 if (media && media.attributes && media.attributes.AUDIO) { 525 if (videoPlaylist.attributes && videoPlaylist.attributes.AUDIO) {
316 mediaGroup = media.attributes.AUDIO; 526 result = this.audioGroups_[videoPlaylist.attributes.AUDIO];
317 } 527 }
318 528
319 return mediaGroup; 529 return result || this.audioGroups_.main;
320 } 530 }
321 531
322 /** 532 /**
323 * Use any audio track that we have, and start to load it 533 * Determine the correct audio rendition based on the active
534 * AudioTrack and initialize a PlaylistLoader and SegmentLoader if
535 * necessary. This method is called once automatically before
536 * playback begins to enable the default audio track and should be
537 * invoked again if the track is changed.
324 */ 538 */
325 useAudio() { 539 setupAudio() {
326 let track; 540 // determine whether seperate loaders are required for the audio
541 // rendition
542 let audioGroup = this.activeAudioGroup();
543 let track = audioGroup.filter((audioTrack) => {
544 return audioTrack.enabled;
545 })[0];
327 546
328 this.audioTracks_.forEach((t) => {
329 if (!track && t.enabled) {
330 track = t;
331 }
332 });
333
334 // called too early or no track is enabled
335 if (!track) { 547 if (!track) {
336 return; 548 track = audioGroup.filter((audioTrack) => {
549 return audioTrack.properties_.default;
550 })[0] || audioGroup[0];
551 track.enabled = true;
337 } 552 }
338 553
339 // Pause any alternative audio 554 // stop playlist and segment loading for audio
340 if (this.audioPlaylistLoader_) { 555 if (this.audioPlaylistLoader_) {
341 this.audioPlaylistLoader_.pause(); 556 this.audioPlaylistLoader_.dispose();
342 this.audioPlaylistLoader_ = null; 557 this.audioPlaylistLoader_ = null;
343 this.audioSegmentLoader_.pause();
344 } 558 }
559 this.audioSegmentLoader_.pause();
560 this.audioSegmentLoader_.clearBuffer();
345 561
346 // If the audio track for the active audio group has 562 if (!track.properties_.resolvedUri) {
347 // a playlist loader than it is an alterative audio track
348 // otherwise it is a part of the mainSegmenLoader
349 let loader = track.getLoader(this.activeAudioGroup());
350
351 if (!loader) {
352 this.mainSegmentLoader_.clearBuffer();
353 return; 563 return;
354 } 564 }
355 565
356 // TODO: it may be better to create the playlist loader here 566 // startup playlist and segment loaders for the enabled audio
357 // when we can change an audioPlaylistLoaders src 567 // track
358 this.audioPlaylistLoader_ = loader; 568 this.audioPlaylistLoader_ = new PlaylistLoader(track.properties_.resolvedUri,
359 569 this.hls_,
360 if (this.audioPlaylistLoader_.started) { 570 this.withCredentials);
361 this.audioPlaylistLoader_.load(); 571 this.audioPlaylistLoader_.start();
362 this.audioSegmentLoader_.load();
363 this.audioSegmentLoader_.clearBuffer();
364 return;
365 }
366 572
367 this.audioPlaylistLoader_.on('loadedmetadata', () => { 573 this.audioPlaylistLoader_.on('loadedmetadata', () => {
368 /* eslint-disable no-shadow */ 574 let audioPlaylist = this.audioPlaylistLoader_.media();
369 let media = this.audioPlaylistLoader_.media();
370 /* eslint-enable no-shadow */
371 575
372 this.audioSegmentLoader_.playlist(media, this.requestOptions_); 576 this.audioSegmentLoader_.playlist(audioPlaylist, this.requestOptions_);
373 this.addMimeType_(this.audioSegmentLoader_, 'mp4a.40.2', media);
374 577
375 // if the video is already playing, or if this isn't a live video and preload 578 // if the video is already playing, or if this isn't a live video and preload
376 // permits, start downloading segments 579 // permits, start downloading segments
377 if (!this.tech_.paused() || 580 if (!this.tech_.paused() ||
378 (media.endList && this.tech_.preload() !== 'none')) { 581 (audioPlaylist.endList && this.tech_.preload() !== 'none')) {
379 this.audioSegmentLoader_.load(); 582 this.audioSegmentLoader_.load();
380 } 583 }
381 584
382 if (!media.endList) { 585 if (!audioPlaylist.endList) {
383 // trigger the playlist loader to start "expired time"-tracking 586 // trigger the playlist loader to start "expired time"-tracking
384 this.audioPlaylistLoader_.trigger('firstplay'); 587 this.audioPlaylistLoader_.trigger('firstplay');
385 } 588 }
...@@ -406,12 +609,8 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -406,12 +609,8 @@ export default class MasterPlaylistController extends videojs.EventTarget {
406 videojs.log.warn('Problem encountered loading the alternate audio track' + 609 videojs.log.warn('Problem encountered loading the alternate audio track' +
407 '. Switching back to default.'); 610 '. Switching back to default.');
408 this.audioSegmentLoader_.abort(); 611 this.audioSegmentLoader_.abort();
409 this.audioPlaylistLoader_ = null; 612 this.setupAudio();
410 this.useAudio();
411 }); 613 });
412
413 this.audioSegmentLoader_.clearBuffer();
414 this.audioPlaylistLoader_.start();
415 } 614 }
416 615
417 /** 616 /**
...@@ -502,7 +701,12 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -502,7 +701,12 @@ export default class MasterPlaylistController extends videojs.EventTarget {
502 // Only attempt to create the source buffer if none already exist. 701 // Only attempt to create the source buffer if none already exist.
503 // handleSourceOpen is also called when we are "re-opening" a source buffer 702 // handleSourceOpen is also called when we are "re-opening" a source buffer
504 // after `endOfStream` has been called (in response to a seek for instance) 703 // after `endOfStream` has been called (in response to a seek for instance)
505 this.setupSourceBuffer_(); 704 try {
705 this.setupSourceBuffers_();
706 } catch (e) {
707 videojs.log.warn('Failed to create Source Buffers', e);
708 return this.mediaSource.endOfStream('decode');
709 }
506 710
507 // if autoplay is enabled, begin playback. This is duplicative of 711 // if autoplay is enabled, begin playback. This is duplicative of
508 // code in video.js but is required because play() must be invoked 712 // code in video.js but is required because play() must be invoked
...@@ -707,11 +911,8 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -707,11 +911,8 @@ export default class MasterPlaylistController extends videojs.EventTarget {
707 */ 911 */
708 dispose() { 912 dispose() {
709 this.masterPlaylistLoader_.dispose(); 913 this.masterPlaylistLoader_.dispose();
710 this.audioTracks_.forEach((track) => {
711 track.dispose();
712 });
713 this.audioTracks_.length = 0;
714 this.mainSegmentLoader_.dispose(); 914 this.mainSegmentLoader_.dispose();
915
715 this.audioSegmentLoader_.dispose(); 916 this.audioSegmentLoader_.dispose();
716 } 917 }
717 918
...@@ -739,8 +940,9 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -739,8 +940,9 @@ export default class MasterPlaylistController extends videojs.EventTarget {
739 * 940 *
740 * @private 941 * @private
741 */ 942 */
742 setupSourceBuffer_() { 943 setupSourceBuffers_() {
743 let media = this.masterPlaylistLoader_.media(); 944 let media = this.masterPlaylistLoader_.media();
945 let mimeTypes;
744 946
745 // wait until a media playlist is available and the Media Source is 947 // wait until a media playlist is available and the Media Source is
746 // attached 948 // attached
...@@ -748,7 +950,17 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -748,7 +950,17 @@ export default class MasterPlaylistController extends videojs.EventTarget {
748 return; 950 return;
749 } 951 }
750 952
751 this.addMimeType_(this.mainSegmentLoader_, 'avc1.4d400d, mp4a.40.2', media); 953 mimeTypes = mimeTypesForPlaylist_(this.masterPlaylistLoader_.master, media);
954 if (mimeTypes.length < 1) {
955 this.error =
956 'No compatible SourceBuffer configuration for the variant stream:' +
957 media.resolvedUri;
958 return this.mediaSource.endOfStream('decode');
959 }
960 this.mainSegmentLoader_.mimeType(mimeTypes[0]);
961 if (mimeTypes[1]) {
962 this.audioSegmentLoader_.mimeType(mimeTypes[1]);
963 }
752 964
753 // exclude any incompatible variant streams from future playlist 965 // exclude any incompatible variant streams from future playlist
754 // selection 966 // selection
...@@ -756,27 +968,6 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -756,27 +968,6 @@ export default class MasterPlaylistController extends videojs.EventTarget {
756 } 968 }
757 969
758 /** 970 /**
759 * add a time type to a segmentLoader
760 *
761 * @param {SegmentLoader} segmentLoader the segmentloader to work on
762 * @param {String} codecs to use by default
763 * @param {Object} the parsed media object
764 * @private
765 */
766 addMimeType_(segmentLoader, defaultCodecs, media) {
767 let mimeType = 'video/mp2t';
768
769 // if the codecs were explicitly specified, pass them along to the
770 // source buffer
771 if (media.attributes && media.attributes.CODECS) {
772 mimeType += '; codecs="' + media.attributes.CODECS + '"';
773 } else {
774 mimeType += '; codecs="' + defaultCodecs + '"';
775 }
776 segmentLoader.mimeType(mimeType);
777 }
778
779 /**
780 * Blacklist playlists that are known to be codec or 971 * Blacklist playlists that are known to be codec or
781 * stream-incompatible with the SourceBuffer configuration. For 972 * stream-incompatible with the SourceBuffer configuration. For
782 * instance, Media Source Extensions would cause the video element to 973 * instance, Media Source Extensions would cause the video element to
...@@ -811,7 +1002,12 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -811,7 +1002,12 @@ export default class MasterPlaylistController extends videojs.EventTarget {
811 }; 1002 };
812 1003
813 if (variant.attributes && variant.attributes.CODECS) { 1004 if (variant.attributes && variant.attributes.CODECS) {
814 variantCodecs = parseCodecs(variant.attributes.CODECS); 1005 let codecString = variant.attributes.CODECS;
1006
1007 variantCodecs = parseCodecs(codecString);
1008 if (!MediaSource.isTypeSupported('video/mp4; codecs="' + codecString + '"')) {
1009 variant.excludeUntil = Infinity;
1010 }
815 } 1011 }
816 1012
817 // if the streams differ in the presence or absence of audio or 1013 // if the streams differ in the presence or absence of audio or
...@@ -831,6 +1027,7 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -831,6 +1027,7 @@ export default class MasterPlaylistController extends videojs.EventTarget {
831 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) { 1027 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
832 variant.excludeUntil = Infinity; 1028 variant.excludeUntil = Infinity;
833 } 1029 }
1030
834 }); 1031 });
835 } 1032 }
836 1033
......
...@@ -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
......
...@@ -6,6 +6,7 @@ import {getMediaIndexForTime_ as getMediaIndexForTime, duration} from './playlis ...@@ -6,6 +6,7 @@ import {getMediaIndexForTime_ as getMediaIndexForTime, duration} from './playlis
6 import videojs from 'video.js'; 6 import videojs from 'video.js';
7 import SourceUpdater from './source-updater'; 7 import SourceUpdater from './source-updater';
8 import {Decrypter} from 'aes-decrypter'; 8 import {Decrypter} from 'aes-decrypter';
9 import mp4probe from 'mux.js/lib/mp4/probe';
9 import Config from './config'; 10 import Config from './config';
10 import window from 'global/window'; 11 import window from 'global/window';
11 12
...@@ -105,6 +106,21 @@ const segmentXhrHeaders = function(segment) { ...@@ -105,6 +106,21 @@ const segmentXhrHeaders = function(segment) {
105 }; 106 };
106 107
107 /** 108 /**
109 * Returns a unique string identifier for a media initialization
110 * segment.
111 */
112 const initSegmentId = function(initSegment) {
113 let byterange = initSegment.byterange || {
114 length: Infinity,
115 offset: 0
116 };
117
118 return [
119 byterange.length, byterange.offset, initSegment.resolvedUri
120 ].join(',');
121 };
122
123 /**
108 * An object that manages segment loading and appending. 124 * An object that manages segment loading and appending.
109 * 125 *
110 * @class SegmentLoader 126 * @class SegmentLoader
...@@ -134,23 +150,31 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -134,23 +150,31 @@ export default class SegmentLoader extends videojs.EventTarget {
134 this.roundTrip = NaN; 150 this.roundTrip = NaN;
135 this.resetStats_(); 151 this.resetStats_();
136 152
137 // private properties 153 // private settings
138 this.hasPlayed_ = settings.hasPlayed; 154 this.hasPlayed_ = settings.hasPlayed;
139 this.currentTime_ = settings.currentTime; 155 this.currentTime_ = settings.currentTime;
140 this.seekable_ = settings.seekable; 156 this.seekable_ = settings.seekable;
141 this.seeking_ = settings.seeking; 157 this.seeking_ = settings.seeking;
142 this.setCurrentTime_ = settings.setCurrentTime; 158 this.setCurrentTime_ = settings.setCurrentTime;
143 this.mediaSource_ = settings.mediaSource; 159 this.mediaSource_ = settings.mediaSource;
160
161 this.hls_ = settings.hls;
162
163 // private instance variables
144 this.checkBufferTimeout_ = null; 164 this.checkBufferTimeout_ = null;
145 this.error_ = void 0; 165 this.error_ = void 0;
146 this.expired_ = 0; 166 this.expired_ = 0;
147 this.timeCorrection_ = 0; 167 this.timeCorrection_ = 0;
148 this.currentTimeline_ = -1; 168 this.currentTimeline_ = -1;
169 this.zeroOffset_ = NaN;
149 this.xhr_ = null; 170 this.xhr_ = null;
150 this.pendingSegment_ = null; 171 this.pendingSegment_ = null;
172 this.mimeType_ = null;
151 this.sourceUpdater_ = null; 173 this.sourceUpdater_ = null;
152 this.hls_ = settings.hls;
153 this.xhrOptions_ = null; 174 this.xhrOptions_ = null;
175
176 this.activeInitSegmentId_ = null;
177 this.initSegments_ = {};
154 } 178 }
155 179
156 /** 180 /**
...@@ -214,6 +238,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -214,6 +238,7 @@ export default class SegmentLoader extends videojs.EventTarget {
214 * load a playlist and start to fill the buffer 238 * load a playlist and start to fill the buffer
215 */ 239 */
216 load() { 240 load() {
241 // un-pause
217 this.monitorBuffer_(); 242 this.monitorBuffer_();
218 243
219 // if we don't have a playlist yet, keep waiting for one to be 244 // if we don't have a playlist yet, keep waiting for one to be
...@@ -222,6 +247,11 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -222,6 +247,11 @@ export default class SegmentLoader extends videojs.EventTarget {
222 return; 247 return;
223 } 248 }
224 249
250 // if all the configuration is ready, initialize and begin loading
251 if (this.state === 'INIT' && this.mimeType_) {
252 return this.init_();
253 }
254
225 // if we're in the middle of processing a segment already, don't 255 // if we're in the middle of processing a segment already, don't
226 // kick off an additional segment request 256 // kick off an additional segment request
227 if (!this.sourceUpdater_ || 257 if (!this.sourceUpdater_ ||
...@@ -240,17 +270,17 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -240,17 +270,17 @@ export default class SegmentLoader extends videojs.EventTarget {
240 * @param {PlaylistLoader} media the playlist to set on the segment loader 270 * @param {PlaylistLoader} media the playlist to set on the segment loader
241 */ 271 */
242 playlist(media, options = {}) { 272 playlist(media, options = {}) {
273 if (!media) {
274 return;
275 }
276
243 this.playlist_ = media; 277 this.playlist_ = media;
244 this.xhrOptions_ = options; 278 this.xhrOptions_ = options;
245 279
246 // if we were unpaused but waiting for a playlist, start 280 // if we were unpaused but waiting for a playlist, start
247 // buffering now 281 // buffering now
248 if (this.sourceUpdater_ && 282 if (this.mimeType_ && this.state === 'INIT' && !this.paused()) {
249 media && 283 return this.init_();
250 this.state === 'INIT' &&
251 !this.paused()) {
252 this.state = 'READY';
253 return this.fillBuffer_();
254 } 284 }
255 } 285 }
256 286
...@@ -293,24 +323,23 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -293,24 +323,23 @@ export default class SegmentLoader extends videojs.EventTarget {
293 * @param {String} mimeType the mime type string to use 323 * @param {String} mimeType the mime type string to use
294 */ 324 */
295 mimeType(mimeType) { 325 mimeType(mimeType) {
296 // TODO Allow source buffers to be re-created with different mime-types 326 if (this.mimeType_) {
297 if (!this.sourceUpdater_) { 327 return;
298 this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, mimeType); 328 }
299 this.clearBuffer();
300 329
330 this.mimeType_ = mimeType;
301 // if we were unpaused but waiting for a sourceUpdater, start 331 // if we were unpaused but waiting for a sourceUpdater, start
302 // buffering now 332 // buffering now
303 if (this.playlist_ && 333 if (this.playlist_ &&
304 this.state === 'INIT' && 334 this.state === 'INIT' &&
305 !this.paused()) { 335 !this.paused()) {
306 this.state = 'READY'; 336 this.init_();
307 return this.fillBuffer_();
308 }
309 } 337 }
310 } 338 }
311 339
312 /** 340 /**
313 * asynchronously/recursively monitor the buffer 341 * As long as the SegmentLoader is in the READY state, periodically
342 * invoke fillBuffer_().
314 * 343 *
315 * @private 344 * @private
316 */ 345 */
...@@ -350,7 +379,6 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -350,7 +379,6 @@ export default class SegmentLoader extends videojs.EventTarget {
350 379
351 let bufferedTime; 380 let bufferedTime;
352 let currentBufferedEnd; 381 let currentBufferedEnd;
353 let timestampOffset = this.sourceUpdater_.timestampOffset();
354 let segment; 382 let segment;
355 let mediaIndex; 383 let mediaIndex;
356 384
...@@ -390,21 +418,6 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -390,21 +418,6 @@ export default class SegmentLoader extends videojs.EventTarget {
390 } 418 }
391 419
392 segment = playlist.segments[mediaIndex]; 420 segment = playlist.segments[mediaIndex];
393 let startOfSegment = duration(playlist,
394 playlist.mediaSequence + mediaIndex,
395 this.expired_);
396
397 // We will need to change timestampOffset of the sourceBuffer if either of
398 // the following conditions are true:
399 // - The segment.timeline !== this.currentTimeline
400 // (we are crossing a discontinuity somehow)
401 // - The "timestampOffset" for the start of this segment is less than
402 // the currently set timestampOffset
403 if (segment.timeline !== this.currentTimeline_ ||
404 startOfSegment < this.sourceUpdater_.timestampOffset()) {
405 timestampOffset = startOfSegment;
406 }
407
408 return { 421 return {
409 // resolve the segment URL relative to the playlist 422 // resolve the segment URL relative to the playlist
410 uri: segment.resolvedUri, 423 uri: segment.resolvedUri,
...@@ -422,7 +435,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -422,7 +435,7 @@ export default class SegmentLoader extends videojs.EventTarget {
422 buffered: null, 435 buffered: null,
423 // The target timestampOffset for this segment when we append it 436 // The target timestampOffset for this segment when we append it
424 // to the source buffer 437 // to the source buffer
425 timestampOffset, 438 timestampOffset: NaN,
426 // The timeline that the segment is in 439 // The timeline that the segment is in
427 timeline: segment.timeline, 440 timeline: segment.timeline,
428 // The expected duration of the segment in seconds 441 // The expected duration of the segment in seconds
...@@ -445,6 +458,18 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -445,6 +458,18 @@ export default class SegmentLoader extends videojs.EventTarget {
445 } 458 }
446 459
447 /** 460 /**
461 * Once all the starting parameters have been specified, begin
462 * operation. This method should only be invoked from the INIT
463 * state.
464 */
465 init_() {
466 this.state = 'READY';
467 this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, this.mimeType_);
468 this.clearBuffer();
469 return this.fillBuffer_();
470 }
471
472 /**
448 * fill the buffer with segements unless the 473 * fill the buffer with segements unless the
449 * sourceBuffers are currently updating 474 * sourceBuffers are currently updating
450 * 475 *
...@@ -456,26 +481,37 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -456,26 +481,37 @@ export default class SegmentLoader extends videojs.EventTarget {
456 } 481 }
457 482
458 // see if we need to begin loading immediately 483 // see if we need to begin loading immediately
459 let request = this.checkBuffer_(this.sourceUpdater_.buffered(), 484 let segmentInfo = this.checkBuffer_(this.sourceUpdater_.buffered(),
460 this.playlist_, 485 this.playlist_,
461 this.currentTime_(), 486 this.currentTime_());
462 this.timestampOffset_);
463 487
464 if (!request) { 488 if (!segmentInfo) {
465 return; 489 return;
466 } 490 }
467 491
468 if (request.mediaIndex === this.playlist_.segments.length - 1 && 492 if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 &&
469 this.mediaSource_.readyState === 'ended' && 493 this.mediaSource_.readyState === 'ended' &&
470 !this.seeking_()) { 494 !this.seeking_()) {
471 return; 495 return;
472 } 496 }
473 497
474 let segment = this.playlist_.segments[request.mediaIndex]; 498 let segment = this.playlist_.segments[segmentInfo.mediaIndex];
475 let startOfSegment = duration(this.playlist_, 499 let startOfSegment = duration(this.playlist_,
476 this.playlist_.mediaSequence + request.mediaIndex, 500 this.playlist_.mediaSequence + segmentInfo.mediaIndex,
477 this.expired_); 501 this.expired_);
478 502
503 // We will need to change timestampOffset of the sourceBuffer if either of
504 // the following conditions are true:
505 // - The segment.timeline !== this.currentTimeline
506 // (we are crossing a discontinuity)
507 // - The "timestampOffset" for the start of this segment is less than
508 // the currently set timestampOffset
509 segmentInfo.timestampOffset = this.sourceUpdater_.timestampOffset();
510 if (segment.timeline !== this.currentTimeline_ ||
511 startOfSegment < this.sourceUpdater_.timestampOffset()) {
512 segmentInfo.timestampOffset = startOfSegment;
513 }
514
479 // Sanity check the segment-index determining logic by calcuating the 515 // Sanity check the segment-index determining logic by calcuating the
480 // percentage of the chosen segment that is buffered. If more than 90% 516 // percentage of the chosen segment that is buffered. If more than 90%
481 // of the segment is buffered then fetching it will likely not help in 517 // of the segment is buffered then fetching it will likely not help in
...@@ -499,7 +535,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -499,7 +535,7 @@ export default class SegmentLoader extends videojs.EventTarget {
499 return; 535 return;
500 } 536 }
501 537
502 this.loadSegment_(request); 538 this.loadSegment_(segmentInfo);
503 } 539 }
504 540
505 /** 541 /**
...@@ -565,6 +601,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -565,6 +601,7 @@ export default class SegmentLoader extends videojs.EventTarget {
565 loadSegment_(segmentInfo) { 601 loadSegment_(segmentInfo) {
566 let segment; 602 let segment;
567 let keyXhr; 603 let keyXhr;
604 let initSegmentXhr;
568 let segmentXhr; 605 let segmentXhr;
569 let removeToTime = 0; 606 let removeToTime = 0;
570 607
...@@ -576,6 +613,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -576,6 +613,7 @@ export default class SegmentLoader extends videojs.EventTarget {
576 613
577 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; 614 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
578 615
616 // optionally, request the decryption key
579 if (segment.key) { 617 if (segment.key) {
580 let keyRequestOptions = videojs.mergeOptions(this.xhrOptions_, { 618 let keyRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
581 uri: segment.key.resolvedUri, 619 uri: segment.key.resolvedUri,
...@@ -585,6 +623,18 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -585,6 +623,18 @@ export default class SegmentLoader extends videojs.EventTarget {
585 keyXhr = this.hls_.xhr(keyRequestOptions, this.handleResponse_.bind(this)); 623 keyXhr = this.hls_.xhr(keyRequestOptions, this.handleResponse_.bind(this));
586 } 624 }
587 625
626 // optionally, request the associated media init segment
627 if (segment.map &&
628 !this.initSegments_[initSegmentId(segment.map)]) {
629 let initSegmentOptions = videojs.mergeOptions(this.xhrOptions_, {
630 uri: segment.map.resolvedUri,
631 responseType: 'arraybuffer',
632 headers: segmentXhrHeaders(segment.map)
633 });
634
635 initSegmentXhr = this.hls_.xhr(initSegmentOptions,
636 this.handleResponse_.bind(this));
637 }
588 this.pendingSegment_ = segmentInfo; 638 this.pendingSegment_ = segmentInfo;
589 639
590 let segmentRequestOptions = videojs.mergeOptions(this.xhrOptions_, { 640 let segmentRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
...@@ -597,6 +647,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -597,6 +647,7 @@ export default class SegmentLoader extends videojs.EventTarget {
597 647
598 this.xhr_ = { 648 this.xhr_ = {
599 keyXhr, 649 keyXhr,
650 initSegmentXhr,
600 segmentXhr, 651 segmentXhr,
601 abort() { 652 abort() {
602 if (this.segmentXhr) { 653 if (this.segmentXhr) {
...@@ -605,6 +656,12 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -605,6 +656,12 @@ export default class SegmentLoader extends videojs.EventTarget {
605 this.segmentXhr.abort(); 656 this.segmentXhr.abort();
606 this.segmentXhr = null; 657 this.segmentXhr = null;
607 } 658 }
659 if (this.initSegmentXhr) {
660 // Prevent error handler from running.
661 this.initSegmentXhr.onreadystatechange = null;
662 this.initSegmentXhr.abort();
663 this.initSegmentXhr = null;
664 }
608 if (this.keyXhr) { 665 if (this.keyXhr) {
609 // Prevent error handler from running. 666 // Prevent error handler from running.
610 this.keyXhr.onreadystatechange = null; 667 this.keyXhr.onreadystatechange = null;
...@@ -630,7 +687,9 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -630,7 +687,9 @@ export default class SegmentLoader extends videojs.EventTarget {
630 687
631 // timeout of previously aborted request 688 // timeout of previously aborted request
632 if (!this.xhr_ || 689 if (!this.xhr_ ||
633 (request !== this.xhr_.segmentXhr && request !== this.xhr_.keyXhr)) { 690 (request !== this.xhr_.segmentXhr &&
691 request !== this.xhr_.keyXhr &&
692 request !== this.xhr_.initSegmentXhr)) {
634 return; 693 return;
635 } 694 }
636 695
...@@ -721,7 +780,14 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -721,7 +780,14 @@ export default class SegmentLoader extends videojs.EventTarget {
721 ]); 780 ]);
722 } 781 }
723 782
724 if (!this.xhr_.segmentXhr && !this.xhr_.keyXhr) { 783 if (request === this.xhr_.initSegmentXhr) {
784 // the init segment request is no longer outstanding
785 this.xhr_.initSegmentXhr = null;
786 segment.map.bytes = new Uint8Array(request.response);
787 this.initSegments_[initSegmentId(segment.map)] = segment.map;
788 }
789
790 if (!this.xhr_.segmentXhr && !this.xhr_.keyXhr && !this.xhr_.initSegmentXhr) {
725 this.xhr_ = null; 791 this.xhr_ = null;
726 this.processResponse_(); 792 this.processResponse_();
727 } 793 }
...@@ -751,6 +817,18 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -751,6 +817,18 @@ export default class SegmentLoader extends videojs.EventTarget {
751 segmentInfo = this.pendingSegment_; 817 segmentInfo = this.pendingSegment_;
752 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; 818 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
753 819
820 // some videos don't start from presentation time zero
821 // if that is the case, set the timestamp offset on the first
822 // segment to adjust them so that it is not necessary to seek
823 // before playback can begin
824 if (segment.map && isNaN(this.zeroOffset_)) {
825 let timescales = mp4probe.timescale(segment.map.bytes);
826 let startTime = mp4probe.startTime(timescales, segmentInfo.bytes);
827
828 this.zeroOffset_ = startTime;
829 segmentInfo.timestampOffset -= startTime;
830 }
831
754 if (segment.key) { 832 if (segment.key) {
755 // this is an encrypted segment 833 // this is an encrypted segment
756 // incrementally decrypt the segment 834 // incrementally decrypt the segment
...@@ -776,16 +854,33 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -776,16 +854,33 @@ export default class SegmentLoader extends videojs.EventTarget {
776 */ 854 */
777 handleSegment_() { 855 handleSegment_() {
778 let segmentInfo; 856 let segmentInfo;
857 let segment;
779 858
780 this.state = 'APPENDING'; 859 this.state = 'APPENDING';
781 segmentInfo = this.pendingSegment_; 860 segmentInfo = this.pendingSegment_;
782 segmentInfo.buffered = this.sourceUpdater_.buffered(); 861 segmentInfo.buffered = this.sourceUpdater_.buffered();
862 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
783 this.currentTimeline_ = segmentInfo.timeline; 863 this.currentTimeline_ = segmentInfo.timeline;
784 864
785 if (segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) { 865 if (segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
786 this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset); 866 this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
787 } 867 }
788 868
869 // if the media initialization segment is changing, append it
870 // before the content segment
871 if (segment.map) {
872 let initId = initSegmentId(segment.map);
873
874 if (!this.activeInitSegmentId_ ||
875 this.activeInitSegmentId_ !== initId) {
876 let initSegment = this.initSegments_[initId];
877
878 this.sourceUpdater_.appendBuffer(initSegment.bytes, () => {
879 this.activeInitSegmentId_ = initId;
880 });
881 }
882 }
883
789 this.sourceUpdater_.appendBuffer(segmentInfo.bytes, 884 this.sourceUpdater_.appendBuffer(segmentInfo.bytes,
790 this.handleUpdateEnd_.bind(this)); 885 this.handleUpdateEnd_.bind(this));
791 } 886 }
......
...@@ -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) => { 412 this.masterPlaylistController_.on('selectedinitialmedia', () => {
440 if (!videojs.browser.IS_FIREFOX || 413 // Add the manual rendition mix-in to HlsHandler
441 !this.audioInfo_ || 414 renditionSelectionMixin(this);
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 }); 415 });
464 416
465 // they did not switch audiotracks 417 this.masterPlaylistController_.on('audioupdate', () => {
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', () => {
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 }
......
...@@ -90,6 +90,9 @@ QUnit.module('HLS', { ...@@ -90,6 +90,9 @@ QUnit.module('HLS', {
90 this.old.Decrypt = videojs.Hls.Decrypter; 90 this.old.Decrypt = videojs.Hls.Decrypter;
91 videojs.Hls.Decrypter = function() {}; 91 videojs.Hls.Decrypter = function() {};
92 92
93 // save and restore browser detection for the Firefox-specific tests
94 this.old.IS_FIREFOX = videojs.browser.IS_FIREFOX;
95
93 // setup a player 96 // setup a player
94 this.player = createPlayer(); 97 this.player = createPlayer();
95 }, 98 },
...@@ -104,6 +107,7 @@ QUnit.module('HLS', { ...@@ -104,6 +107,7 @@ QUnit.module('HLS', {
104 107
105 videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport; 108 videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport;
106 videojs.Hls.Decrypter = this.old.Decrypt; 109 videojs.Hls.Decrypter = this.old.Decrypt;
110 videojs.browser.IS_FIREFOX = this.old.IS_FIREFOX;
107 111
108 this.player.dispose(); 112 this.player.dispose();
109 } 113 }
...@@ -260,11 +264,11 @@ QUnit.test('codecs are passed to the source buffer', function() { ...@@ -260,11 +264,11 @@ QUnit.test('codecs are passed to the source buffer', function() {
260 264
261 this.requests.shift().respond(200, null, 265 this.requests.shift().respond(200, null,
262 '#EXTM3U\n' + 266 '#EXTM3U\n' +
263 '#EXT-X-STREAM-INF:CODECS="video, audio"\n' + 267 '#EXT-X-STREAM-INF:CODECS="avc1.dd00dd, mp4a.40.f"\n' +
264 'media.m3u8\n'); 268 'media.m3u8\n');
265 standardXHRResponse(this.requests.shift()); 269 standardXHRResponse(this.requests.shift());
266 QUnit.equal(codecs.length, 1, 'created a source buffer'); 270 QUnit.equal(codecs.length, 1, 'created a source buffer');
267 QUnit.equal(codecs[0], 'video/mp2t; codecs="video, audio"', 'specified the codecs'); 271 QUnit.equal(codecs[0], 'video/mp2t; codecs="avc1.dd00dd, mp4a.40.f"', 'specified the codecs');
268 }); 272 });
269 273
270 QUnit.test('including HLS as a tech does not error', function() { 274 QUnit.test('including HLS as a tech does not error', function() {
...@@ -862,7 +866,7 @@ QUnit.test('does not blacklist compatible AAC codec strings', function() { ...@@ -862,7 +866,7 @@ QUnit.test('does not blacklist compatible AAC codec strings', function() {
862 '#EXTM3U\n' + 866 '#EXTM3U\n' +
863 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.2"\n' + 867 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.2"\n' +
864 'media.m3u8\n' + 868 'media.m3u8\n' +
865 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.3"\n' + 869 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,not-an-audio-codec"\n' +
866 'media1.m3u8\n'); 870 'media1.m3u8\n');
867 871
868 // media 872 // media
...@@ -870,10 +874,10 @@ QUnit.test('does not blacklist compatible AAC codec strings', function() { ...@@ -870,10 +874,10 @@ QUnit.test('does not blacklist compatible AAC codec strings', function() {
870 master = this.player.tech_.hls.playlists.master; 874 master = this.player.tech_.hls.playlists.master;
871 QUnit.strictEqual(typeof master.playlists[0].excludeUntil, 875 QUnit.strictEqual(typeof master.playlists[0].excludeUntil,
872 'undefined', 876 'undefined',
873 'did not blacklist'); 877 'did not blacklist mp4a.40.2');
874 QUnit.strictEqual(typeof master.playlists[1].excludeUntil, 878 QUnit.strictEqual(master.playlists[1].excludeUntil,
875 'undefined', 879 Infinity,
876 'did not blacklist'); 880 'blacklisted invalid audio codec');
877 881
878 // verify stats 882 // verify stats
879 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above'); 883 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
...@@ -1453,6 +1457,7 @@ QUnit.test('re-emits mediachange events', function() { ...@@ -1453,6 +1457,7 @@ QUnit.test('re-emits mediachange events', function() {
1453 type: 'application/vnd.apple.mpegurl' 1457 type: 'application/vnd.apple.mpegurl'
1454 }); 1458 });
1455 openMediaSource(this.player, this.clock); 1459 openMediaSource(this.player, this.clock);
1460 standardXHRResponse(this.requests.shift());
1456 1461
1457 this.player.tech_.hls.playlists.trigger('mediachange'); 1462 this.player.tech_.hls.playlists.trigger('mediachange');
1458 QUnit.strictEqual(mediaChanges, 1, 'fired mediachange'); 1463 QUnit.strictEqual(mediaChanges, 1, 'fired mediachange');
...@@ -1713,7 +1718,7 @@ QUnit.test('resolves relative key URLs against the playlist', function() { ...@@ -1713,7 +1718,7 @@ QUnit.test('resolves relative key URLs against the playlist', function() {
1713 'resolves the key URL'); 1718 'resolves the key URL');
1714 }); 1719 });
1715 1720
1716 QUnit.test('adds 1 default audio track if we have not parsed any, and the playlist is loaded', function() { 1721 QUnit.test('adds 1 default audio track if we have not parsed any and the playlist is loaded', function() {
1717 this.player.src({ 1722 this.player.src({
1718 src: 'manifest/master.m3u8', 1723 src: 'manifest/master.m3u8',
1719 type: 'application/vnd.apple.mpegurl' 1724 type: 'application/vnd.apple.mpegurl'
...@@ -1725,12 +1730,11 @@ QUnit.test('adds 1 default audio track if we have not parsed any, and the playli ...@@ -1725,12 +1730,11 @@ QUnit.test('adds 1 default audio track if we have not parsed any, and the playli
1725 1730
1726 // master 1731 // master
1727 standardXHRResponse(this.requests.shift()); 1732 standardXHRResponse(this.requests.shift());
1733 // media
1734 standardXHRResponse(this.requests.shift());
1728 1735
1729 QUnit.equal(this.player.audioTracks().length, 1, 'one audio track after load'); 1736 QUnit.equal(this.player.audioTracks().length, 1, 'one audio track after load');
1730 QUnit.ok(this.player.audioTracks()[0] instanceof HlsAudioTrack, 'audio track is an hls audio track'); 1737 QUnit.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
1731
1732 // verify stats
1733 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
1734 }); 1738 });
1735 1739
1736 QUnit.test('adds 1 default audio track if in flash mode', function() { 1740 QUnit.test('adds 1 default audio track if in flash mode', function() {
...@@ -1754,9 +1758,11 @@ QUnit.test('adds 1 default audio track if in flash mode', function() { ...@@ -1754,9 +1758,11 @@ QUnit.test('adds 1 default audio track if in flash mode', function() {
1754 1758
1755 // master 1759 // master
1756 standardXHRResponse(this.requests.shift()); 1760 standardXHRResponse(this.requests.shift());
1761 // media
1762 standardXHRResponse(this.requests.shift());
1757 1763
1758 QUnit.equal(this.player.audioTracks().length, 1, 'one audio track after load'); 1764 QUnit.equal(this.player.audioTracks().length, 1, 'one audio track after load');
1759 QUnit.ok(this.player.audioTracks()[0] instanceof HlsAudioTrack, 'audio track is an hls audio track'); 1765 QUnit.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
1760 1766
1761 videojs.options.hls = hlsOptions; 1767 videojs.options.hls = hlsOptions;
1762 }); 1768 });
...@@ -1773,64 +1779,22 @@ QUnit.test('adds audio tracks if we have parsed some from a playlist', function( ...@@ -1773,64 +1779,22 @@ QUnit.test('adds audio tracks if we have parsed some from a playlist', function(
1773 1779
1774 // master 1780 // master
1775 standardXHRResponse(this.requests.shift()); 1781 standardXHRResponse(this.requests.shift());
1776 let hls = this.player.tech_.hls; 1782 // media
1777 let hlsAudioTracks = hls.masterPlaylistController_.audioTracks_; 1783 standardXHRResponse(this.requests.shift());
1778 let vjsAudioTracks = this.player.audioTracks(); 1784 let vjsAudioTracks = this.player.audioTracks();
1779 1785
1780 QUnit.equal(hlsAudioTracks.length, 3, '3 active hls tracks');
1781 QUnit.equal(vjsAudioTracks.length, 3, '3 active vjs tracks'); 1786 QUnit.equal(vjsAudioTracks.length, 3, '3 active vjs tracks');
1782 1787
1783 QUnit.equal(vjsAudioTracks[0].enabled, true, 'default track is enabled'); 1788 QUnit.equal(vjsAudioTracks[0].enabled, true, 'default track is enabled');
1784 QUnit.equal(hlsAudioTracks[0].enabled, true, 'default track is enabled');
1785 1789
1786 vjsAudioTracks[1].enabled = true; 1790 vjsAudioTracks[1].enabled = true;
1787 QUnit.equal(hlsAudioTracks[1].enabled, true, 'new track is enabled on hls');
1788 QUnit.equal(vjsAudioTracks[1].enabled, true, 'new track is enabled on vjs'); 1791 QUnit.equal(vjsAudioTracks[1].enabled, true, 'new track is enabled on vjs');
1789
1790 QUnit.equal(vjsAudioTracks[0].enabled, false, 'main track is disabled'); 1792 QUnit.equal(vjsAudioTracks[0].enabled, false, 'main track is disabled');
1791 QUnit.equal(hlsAudioTracks[0].enabled, false, 'main track is disabled');
1792
1793 hlsAudioTracks[2].enabled = true;
1794 QUnit.equal(hlsAudioTracks[2].enabled, true, 'new track is enabled on hls');
1795 QUnit.equal(vjsAudioTracks[2].enabled, true, 'new track is enabled on vjs');
1796
1797 QUnit.equal(vjsAudioTracks[1].enabled, false, 'main track is disabled');
1798 QUnit.equal(hlsAudioTracks[1].enabled, false, 'main track is disabled');
1799
1800 // verify stats
1801 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
1802 }); 1793 });
1803 1794
1804 QUnit.test('audio info from audioinfo event is stored on hls', function() { 1795 QUnit.test('when audioinfo changes on an independent audio track in Firefox, the enabled track is blacklisted and removed', function() {
1805 // force non-firefox as firefox has specific behavior 1796 let audioTracks = this.player.audioTracks();
1806 let oldIsFirefox = videojs.browser.IS_FIREFOX; 1797 let oldLabel;
1807
1808 videojs.browser.IS_FIREFOX = false;
1809
1810 this.player.src({
1811 src: 'manifest/multipleAudioGroups.m3u8',
1812 type: 'application/vnd.apple.mpegurl'
1813 });
1814
1815 let hls = this.player.tech_.hls;
1816 let mpc = hls.masterPlaylistController_;
1817 let info = {foo: 'bar'};
1818
1819 QUnit.ok(!hls.audioInfo_, 'hls has no audioInfo_');
1820
1821 mpc.trigger({type: 'audioinfo', info});
1822 QUnit.equal(hls.audioInfo_, info, 'hls has the info from the event');
1823
1824 info = {bar: 'foo'};
1825 mpc.trigger({type: 'audioinfo', info});
1826 QUnit.equal(hls.audioInfo_, info, 'hls has the new info from the event');
1827
1828 videojs.browser.IS_FIREFOX = oldIsFirefox;
1829 });
1830
1831 QUnit.test('audioinfo changes with three tracks, enabled track is blacklisted and removed', function() {
1832 let oldIsFirefox = videojs.browser.IS_FIREFOX;
1833 let at = this.player.audioTracks();
1834 1798
1835 videojs.browser.IS_FIREFOX = true; 1799 videojs.browser.IS_FIREFOX = true;
1836 this.player.src({ 1800 this.player.src({
...@@ -1840,54 +1804,34 @@ QUnit.test('audioinfo changes with three tracks, enabled track is blacklisted an ...@@ -1840,54 +1804,34 @@ QUnit.test('audioinfo changes with three tracks, enabled track is blacklisted an
1840 let hls = this.player.tech_.hls; 1804 let hls = this.player.tech_.hls;
1841 let mpc = hls.masterPlaylistController_; 1805 let mpc = hls.masterPlaylistController_;
1842 1806
1843 QUnit.equal(at.length, 0, 'zero audio tracks at load time');
1844 QUnit.ok(!hls.audioInfo_, 'no audio info on hls');
1845 openMediaSource(this.player, this.clock); 1807 openMediaSource(this.player, this.clock);
1808
1809 // master
1846 standardXHRResponse(this.requests.shift()); 1810 standardXHRResponse(this.requests.shift());
1811 // media
1847 standardXHRResponse(this.requests.shift()); 1812 standardXHRResponse(this.requests.shift());
1848 QUnit.equal(at.length, 3, 'three audio track after load'); 1813 QUnit.equal(audioTracks.length, 3, 'three audio track after load');
1849 QUnit.ok(!hls.audioInfo_, 'no audio info on hls');
1850
1851 let defaultTrack;
1852
1853 mpc.audioTracks_.forEach((t) => {
1854 if (!defaultTrack && t.default) {
1855 defaultTrack = t;
1856 }
1857 });
1858
1859 let blacklistPlaylistCalls = 0;
1860 let info = {foo: 'bar'};
1861 1814
1862 // noop as there is no real playlist 1815 let defaultTrack = mpc.activeAudioGroup().filter((track) => {
1863 mpc.useAudio = () => {}; 1816 return track.properties_.default;
1817 })[0];
1864 1818
1865 // initial audio info 1819 // initial audio info
1866 mpc.trigger({type: 'audioinfo', info}); 1820 hls.mediaSource.trigger({ type: 'audioinfo', info: { foo: 'bar' }});
1867 QUnit.equal(hls.audioInfo_, info, 'hls has the info from the event'); 1821 oldLabel = audioTracks[1].label;
1868 1822
1869 // simulate audio info change and mock things 1823 // simulate audio info change and mock things
1870 let oldLabel = at[1].label; 1824 audioTracks[1].enabled = true;
1825 hls.mediaSource.trigger({ type: 'audioinfo', info: { bar: 'foo' }});
1871 1826
1872 at[1].enabled = true; 1827 QUnit.equal(audioTracks.length, 2, 'two audio tracks after bad audioinfo change');
1873 mpc.blacklistCurrentPlaylist = () => blacklistPlaylistCalls++; 1828 QUnit.notEqual(audioTracks[1].label, oldLabel, 'audio track at index 1 is not the same');
1874 mpc.trigger({type: 'audioinfo', info: {bar: 'foo'}});
1875
1876 QUnit.equal(hls.audioInfo_, info, 'hls did not store the changed audio info');
1877 QUnit.equal(at.length, 2, 'two audio tracks after bad audioinfo change');
1878 QUnit.notEqual(at[1].label, oldLabel, 'audio track at index 1 is not the same');
1879 QUnit.equal(defaultTrack.enabled, true, 'default track is enabled again'); 1829 QUnit.equal(defaultTrack.enabled, true, 'default track is enabled again');
1880 QUnit.equal(blacklistPlaylistCalls, 0, 'blacklist was not called on playlist');
1881 QUnit.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged'); 1830 QUnit.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged');
1882 videojs.browser.IS_FIREFOX = oldIsFirefox;
1883
1884 // verify stats
1885 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
1886 }); 1831 });
1887 1832
1888 QUnit.test('audioinfo changes with one track, blacklist playlist', function() { 1833 QUnit.test('audioinfo changes with one track, blacklist playlist', function() {
1889 let oldIsFirefox = videojs.browser.IS_FIREFOX; 1834 let audioTracks = this.player.audioTracks();
1890 let at = this.player.audioTracks();
1891 1835
1892 videojs.browser.IS_FIREFOX = true; 1836 videojs.browser.IS_FIREFOX = true;
1893 this.player.src({ 1837 this.player.src({
...@@ -1895,35 +1839,28 @@ QUnit.test('audioinfo changes with one track, blacklist playlist', function() { ...@@ -1895,35 +1839,28 @@ QUnit.test('audioinfo changes with one track, blacklist playlist', function() {
1895 type: 'application/vnd.apple.mpegurl' 1839 type: 'application/vnd.apple.mpegurl'
1896 }); 1840 });
1897 1841
1898 QUnit.equal(at.length, 0, 'zero audio tracks at load time'); 1842 QUnit.equal(audioTracks.length, 0, 'zero audio tracks at load time');
1899 openMediaSource(this.player, this.clock); 1843 openMediaSource(this.player, this.clock);
1900 standardXHRResponse(this.requests.shift()); 1844 standardXHRResponse(this.requests.shift());
1901 standardXHRResponse(this.requests.shift()); 1845 standardXHRResponse(this.requests.shift());
1902 QUnit.equal(at.length, 1, 'one audio track after load'); 1846 QUnit.equal(audioTracks.length, 1, 'one audio track after load');
1903 1847
1904 let mpc = this.player.tech_.hls.masterPlaylistController_; 1848 let mpc = this.player.tech_.hls.masterPlaylistController_;
1905 let blacklistPlaylistCalls = 0; 1849 let oldMedia = mpc.media();
1906 1850
1907 mpc.blacklistCurrentPlaylist = () => blacklistPlaylistCalls++; 1851 // initial audio info
1908 // noop as there is no real playlist 1852 mpc.mediaSource.trigger({type: 'audioinfo', info: { foo: 'bar' }});
1909 mpc.useAudio = () => {};
1910 mpc.trigger({type: 'audioinfo', info: {foo: 'bar'}});
1911 1853
1912 // simulate audio info change in main track 1854 // simulate audio info change in main track
1913 mpc.trigger({type: 'audioinfo', info: {bar: 'foo'}}); 1855 mpc.mediaSource.trigger({type: 'audioinfo', info: { bar: 'foo' }});
1914 1856
1915 QUnit.equal(at.length, 1, 'still have one audio track'); 1857 QUnit.equal(audioTracks.length, 1, 'still have one audio track');
1916 QUnit.equal(blacklistPlaylistCalls, 1, 'blacklist was called on playlist'); 1858 QUnit.ok(oldMedia.excludeUntil > 0, 'blacklisted old playlist');
1917 QUnit.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged'); 1859 QUnit.equal(this.env.log.warn.calls, 2, 'firefox issue warning logged');
1918 videojs.browser.IS_FIREFOX = oldIsFirefox;
1919
1920 // verify stats
1921 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
1922 }); 1860 });
1923 1861
1924 QUnit.test('audioinfo changes with three tracks, default is enabled, blacklisted playlist', function() { 1862 QUnit.test('changing audioinfo for muxed audio blacklists the current playlist in Firefox', function() {
1925 let oldIsFirefox = videojs.browser.IS_FIREFOX; 1863 let audioTracks = this.player.audioTracks();
1926 let at = this.player.audioTracks();
1927 1864
1928 videojs.browser.IS_FIREFOX = true; 1865 videojs.browser.IS_FIREFOX = true;
1929 this.player.src({ 1866 this.player.src({
...@@ -1931,45 +1868,48 @@ QUnit.test('audioinfo changes with three tracks, default is enabled, blacklisted ...@@ -1931,45 +1868,48 @@ QUnit.test('audioinfo changes with three tracks, default is enabled, blacklisted
1931 type: 'application/vnd.apple.mpegurl' 1868 type: 'application/vnd.apple.mpegurl'
1932 }); 1869 });
1933 1870
1934 QUnit.equal(at.length, 0, 'zero audio tracks at load time'); 1871 QUnit.equal(audioTracks.length, 0, 'zero audio tracks at load time');
1935 openMediaSource(this.player, this.clock); 1872 openMediaSource(this.player, this.clock);
1936 standardXHRResponse(this.requests.shift());
1937 standardXHRResponse(this.requests.shift());
1938 QUnit.equal(at.length, 3, 'three audio track after load');
1939
1940 let hls = this.player.tech_.hls; 1873 let hls = this.player.tech_.hls;
1941 let mpc = hls.masterPlaylistController_; 1874 let mpc = hls.masterPlaylistController_;
1942 1875
1943 // force audio group with combined audio to enabled 1876 // master
1944 mpc.activeAudioGroup = () => 'audio-lo'; 1877 standardXHRResponse(this.requests.shift());
1945 let defaultTrack; 1878 // video media
1946 1879 standardXHRResponse(this.requests.shift());
1947 mpc.audioTracks_.forEach((t) => { 1880 // video segments
1948 if (!defaultTrack && t.default) { 1881 standardXHRResponse(this.requests.shift());
1949 defaultTrack = t; 1882 standardXHRResponse(this.requests.shift());
1950 } 1883 // audio media
1951 }); 1884 standardXHRResponse(this.requests.shift());
1885 // ignore audio requests
1886 this.requests.length = 0;
1887 QUnit.equal(audioTracks.length, 3, 'three audio track after load');
1952 1888
1953 let blacklistPlaylistCalls = 0; 1889 // force audio group with combined audio to enabled
1890 mpc.masterPlaylistLoader_.media(mpc.master().playlists[0]);
1891 this.requests.shift().respond(200, null,
1892 '#EXTM3U\n' +
1893 '#EXTINF:10,\n' +
1894 '0.ts\n' +
1895 '#EXT-X-ENDLIST\n');
1954 1896
1955 // noop as there is no real playlist 1897 let defaultTrack = mpc.activeAudioGroup().filter((track) => {
1956 mpc.useAudio = () => {}; 1898 return track.properties_.default;
1899 })[0];
1900 let oldPlaylist = mpc.media();
1957 1901
1958 // initial audio info 1902 // initial audio info
1959 mpc.trigger({type: 'audioinfo', info: {foo: 'bar'}}); 1903 mpc.mediaSource.trigger({type: 'audioinfo', info: { foo: 'bar' }});
1960 1904
1961 // simulate audio info change and mock things 1905 // simulate audio info change
1962 mpc.blacklistCurrentPlaylist = () => blacklistPlaylistCalls++; 1906 mpc.mediaSource.trigger({type: 'audioinfo', info: { bar: 'foo' }});
1963 mpc.trigger({type: 'audioinfo', info: {bar: 'foo'}});
1964 1907
1965 QUnit.equal(at.length, 3, 'three audio tracks after bad audioinfo change'); 1908 audioTracks = this.player.audioTracks();
1909 QUnit.equal(audioTracks.length, 3, 'three audio tracks after bad audioinfo change');
1966 QUnit.equal(defaultTrack.enabled, true, 'default audio still enabled'); 1910 QUnit.equal(defaultTrack.enabled, true, 'default audio still enabled');
1967 QUnit.equal(blacklistPlaylistCalls, 1, 'blacklist was called on playlist'); 1911 QUnit.ok(oldPlaylist.excludeUntil > 0, 'blacklisted the old playlist');
1968 QUnit.equal(this.env.log.warn.calls, 1, 'firefox issue warning logged'); 1912 QUnit.equal(this.env.log.warn.calls, 2, 'firefox issue warning logged');
1969 videojs.browser.IS_FIREFOX = oldIsFirefox;
1970
1971 // verify stats
1972 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
1973 }); 1913 });
1974 1914
1975 QUnit.test('cleans up the buffer when loading live segments', function() { 1915 QUnit.test('cleans up the buffer when loading live segments', function() {
...@@ -2125,75 +2065,62 @@ QUnit.test('when mediaGroup changes enabled track should not change', function() ...@@ -2125,75 +2065,62 @@ QUnit.test('when mediaGroup changes enabled track should not change', function()
2125 src: 'manifest/multipleAudioGroups.m3u8', 2065 src: 'manifest/multipleAudioGroups.m3u8',
2126 type: 'application/vnd.apple.mpegurl' 2066 type: 'application/vnd.apple.mpegurl'
2127 }); 2067 });
2128
2129 QUnit.equal(this.player.audioTracks().length, 0, 'zero audio tracks at load time');
2130 openMediaSource(this.player, this.clock); 2068 openMediaSource(this.player, this.clock);
2131 2069
2132 // master 2070 // master
2133 standardXHRResponse(this.requests.shift()); 2071 standardXHRResponse(this.requests.shift());
2072 // video media
2134 standardXHRResponse(this.requests.shift()); 2073 standardXHRResponse(this.requests.shift());
2135 let hls = this.player.tech_.hls; 2074 let hls = this.player.tech_.hls;
2075 let mpc = hls.masterPlaylistController_;
2136 let audioTracks = this.player.audioTracks(); 2076 let audioTracks = this.player.audioTracks();
2137 2077
2138 QUnit.equal(audioTracks.length, 3, 'three audio tracks after load'); 2078 QUnit.equal(audioTracks.length, 3, 'three audio tracks after load');
2139 let trackOne = audioTracks[0]; 2079 QUnit.equal(audioTracks[0].enabled, true, 'track one enabled after load');
2140 let trackTwo = audioTracks[1];
2141 let trackThree = audioTracks[2];
2142
2143 QUnit.equal(trackOne.enabled, true, 'track one enabled after load');
2144 2080
2145 let oldMediaGroup = hls.playlists.media().attributes.AUDIO; 2081 let oldMediaGroup = hls.playlists.media().attributes.AUDIO;
2146 2082
2083 // clear out any outstanding requests
2084 this.requests.length = 0;
2147 // force mpc to select a playlist from a new media group 2085 // force mpc to select a playlist from a new media group
2148 hls.selectPlaylist = () => { 2086 mpc.masterPlaylistLoader_.media(mpc.master().playlists[0]);
2149 let playlist;
2150 2087
2151 hls.playlists.master.playlists.forEach((p) => { 2088 // TODO extra segment requests!!!
2152 if (!playlist && p.attributes.AUDIO !== oldMediaGroup) { 2089 this.requests.shift();
2153 playlist = p; 2090 this.requests.shift();
2154 }
2155 });
2156 return playlist;
2157 };
2158 2091
2159 // select a new mediaGroup 2092 // video media
2160 hls.masterPlaylistController_.blacklistCurrentPlaylist();
2161 while (this.requests.length > 0) {
2162 standardXHRResponse(this.requests.shift()); 2093 standardXHRResponse(this.requests.shift());
2163 } 2094
2164 QUnit.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist'); 2095 QUnit.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
2165 QUnit.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist'); 2096 audioTracks = this.player.audioTracks();
2166 2097
2167 QUnit.equal(audioTracks.length, 3, 'three audio tracks after mediaGroup Change'); 2098 QUnit.equal(audioTracks.length, 3, 'three audio tracks after changing mediaGroup');
2168 QUnit.equal(audioTracks[0], trackOne, 'track one did not change'); 2099 QUnit.ok(audioTracks[0].properties_.default, 'track one should be the default');
2169 QUnit.equal(audioTracks[1], trackTwo, 'track two did not change'); 2100 QUnit.ok(audioTracks[0].enabled, 'enabled the default track');
2170 QUnit.equal(audioTracks[2], trackThree, 'track three did not change'); 2101 QUnit.notOk(audioTracks[1].enabled, 'disabled track two');
2102 QUnit.notOk(audioTracks[2].enabled, 'disabled track three');
2171 2103
2172 trackTwo.enabled = true; 2104 audioTracks[1].enabled = true;
2173 QUnit.equal(trackOne.enabled, false, 'track 1 - now disabled'); 2105 QUnit.notOk(audioTracks[0].enabled, 'disabled track one');
2174 QUnit.equal(trackTwo.enabled, true, 'track 2 - now enabled'); 2106 QUnit.ok(audioTracks[1].enabled, 'enabled track two');
2175 QUnit.equal(trackThree.enabled, false, 'track 3 - disabled'); 2107 QUnit.notOk(audioTracks[2].enabled, 'disabled track three');
2176 2108
2177 oldMediaGroup = hls.playlists.media().attributes.AUDIO; 2109 oldMediaGroup = hls.playlists.media().attributes.AUDIO;
2178 // select a new mediaGroup 2110 // clear out any outstanding requests
2179 hls.masterPlaylistController_.blacklistCurrentPlaylist(); 2111 this.requests.length = 0;
2180 while (this.requests.length > 0) { 2112 // swap back to the old media group
2181 standardXHRResponse(this.requests.shift()); 2113 // this playlist is already loaded so no new requests are made
2182 } 2114 mpc.masterPlaylistLoader_.media(mpc.master().playlists[3]);
2183 QUnit.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
2184 QUnit.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist');
2185
2186 QUnit.equal(audioTracks.length, 3, 'three audio tracks after mediaGroup Change');
2187 QUnit.equal(audioTracks[0], trackOne, 'track one did not change');
2188 QUnit.equal(audioTracks[1], trackTwo, 'track two did not change');
2189 QUnit.equal(audioTracks[2], trackThree, 'track three did not change');
2190 2115
2191 QUnit.equal(trackOne.enabled, false, 'track 1 - still disabled'); 2116 QUnit.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
2192 QUnit.equal(trackTwo.enabled, true, 'track 2 - still enabled'); 2117 audioTracks = this.player.audioTracks();
2193 QUnit.equal(trackThree.enabled, false, 'track 3 - disabled');
2194 2118
2195 // verify stats 2119 QUnit.equal(audioTracks.length, 3, 'three audio tracks after reverting mediaGroup');
2196 QUnit.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default'); 2120 QUnit.ok(audioTracks[0].properties_.default, 'track one should be the default');
2121 QUnit.notOk(audioTracks[0].enabled, 'the default track is still disabled');
2122 QUnit.ok(audioTracks[1].enabled, 'track two is still enabled');
2123 QUnit.notOk(audioTracks[2].enabled, 'track three is still disabled');
2197 }); 2124 });
2198 2125
2199 QUnit.test('Allows specifying the beforeRequest function on the player', function() { 2126 QUnit.test('Allows specifying the beforeRequest function on the player', function() {
......
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
......