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
692 additions
and
112 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", | ... | ... |
This diff is collapsed.
Click to expand it.
... | @@ -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 | ... | ... |
This diff is collapsed.
Click to expand it.
... | @@ -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 | } | ... | ... |
This diff is collapsed.
Click to expand it.
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