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.
Showing
12 changed files
with
1259 additions
and
460 deletions
... | @@ -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() { | ... | ... |
utils/manifest/prog_index.m3u8
0 → 100644
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 | ... | ... |
-
Please register or sign in to post a comment