2cce0c18 by Jon-Carlos Rivera

Multiple audio track support (#681)

* Support for multiple alternate audio tracks

* Separated segment loading logic into a reusable piece of functionality so that we can have more than one segment loader and they can manage the segment fetch behavior unique to each playlist they are loading from

* Introduced the MasterPlaylistController to coordinate the loading of the master playlist and separate the behavior of the player from videojs-contrib-hls.js leaving the latter to be the glue between HLS and the video element's events

* Added the SourceUpdater to manage the asynchronous bahavior of SourceBuffers and MediaSource objects so that we can treat it as a non-blocking work queue

* Added parsing for MediaGroups in master playlists

* Added support for AudioTrackList objects and events

* Add support for every HLS mime type possible (#684)

* Flash live fixes (#682)
1 parent d33786f1
Showing 93 changed files with 7273 additions and 2238 deletions
1 sudo: false 1 sudo: required
2 dist: trusty
2 language: node_js 3 language: node_js
3 addons:
4 firefox: "latest"
5 node_js: 4 node_js:
6 - "stable" 5 - "stable"
7 notifications: 6 notifications:
...@@ -14,6 +13,7 @@ notifications: ...@@ -14,6 +13,7 @@ notifications:
14 use_notice: true 13 use_notice: true
15 # Set up a virtual screen for Firefox. 14 # Set up a virtual screen for Firefox.
16 before_script: 15 before_script:
16 - export CHROME_BIN=/usr/bin/google-chrome
17 - export DISPLAY=:99.0 17 - export DISPLAY=:99.0
18 - sh -e /etc/init.d/xvfb start 18 - sh -e /etc/init.d/xvfb start
19 env: 19 env:
...@@ -22,4 +22,8 @@ env: ...@@ -22,4 +22,8 @@ env:
22 - secure: AnduYGXka5ft1x7V3SuVYqvlKLvJGhUaRNFdy4UDJr3ZVuwpQjE4TMDG8REmJIJvXfHbh4qY4N1cFSGnXkZ4bH21Xk0v9DLhsxbarKz+X2BvPgXs+Af9EQ6vLEy/5S1vMLxfT5+y+Ec5bVNGOsdUZby8Y21CRzSg6ADN9kwPGlE= 22 - secure: AnduYGXka5ft1x7V3SuVYqvlKLvJGhUaRNFdy4UDJr3ZVuwpQjE4TMDG8REmJIJvXfHbh4qY4N1cFSGnXkZ4bH21Xk0v9DLhsxbarKz+X2BvPgXs+Af9EQ6vLEy/5S1vMLxfT5+y+Ec5bVNGOsdUZby8Y21CRzSg6ADN9kwPGlE=
23 addons: 23 addons:
24 sauce_connect: true 24 sauce_connect: true
25 firefox: latest 25 apt:
26 sources:
27 - google-chrome
28 packages:
29 - google-chrome-stable
......
...@@ -9,6 +9,8 @@ Play back HLS with video.js, even where it's not natively supported. ...@@ -9,6 +9,8 @@ Play back HLS with video.js, even where it's not natively supported.
9 **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 9 **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
10 10
11 - [Getting Started](#getting-started) 11 - [Getting Started](#getting-started)
12 - [Known Issues](#known-issues)
13 - [IE11](#ie11)
12 - [Documentation](#documentation) 14 - [Documentation](#documentation)
13 - [Options](#options) 15 - [Options](#options)
14 - [withCredentials](#withcredentials) 16 - [withCredentials](#withcredentials)
...@@ -44,7 +46,7 @@ and include it in your page along with video.js: ...@@ -44,7 +46,7 @@ and include it in your page along with video.js:
44 type="application/x-mpegURL"> 46 type="application/x-mpegURL">
45 </video> 47 </video>
46 <script src="video.js"></script> 48 <script src="video.js"></script>
47 <script src="videojs-hls.min.js"></script> 49 <script src="videojs-contrib-hls.min.js"></script>
48 <script> 50 <script>
49 var player = videojs('example-video'); 51 var player = videojs('example-video');
50 player.play(); 52 player.play();
...@@ -53,6 +55,16 @@ player.play(); ...@@ -53,6 +55,16 @@ player.play();
53 55
54 Check out our [live example](http://videojs.github.io/videojs-contrib-hls/) if you're having trouble. 56 Check out our [live example](http://videojs.github.io/videojs-contrib-hls/) if you're having trouble.
55 57
58 ## Known Issues
59 Issues that are currenty know about with workarounds. If you want to
60 help find a solution that would be appreciated!
61
62 ### IE11
63 In some IE11 setups there are issues working with it's native HTML
64 SourceBuffers functionality. This leads to various issues, such as
65 videos stopping playback with media decode errors. The known workaround
66 for this issues is to force the player to use flash when running on IE11.
67
56 ## Documentation 68 ## Documentation
57 [HTTP Live Streaming](https://developer.apple.com/streaming/) (HLS) has 69 [HTTP Live Streaming](https://developer.apple.com/streaming/) (HLS) has
58 become a de-facto standard for streaming video on mobile devices 70 become a de-facto standard for streaming video on mobile devices
...@@ -89,8 +101,7 @@ are some highlights: ...@@ -89,8 +101,7 @@ are some highlights:
89 - mid-segment quality switching 101 - mid-segment quality switching
90 - AES-128 segment encryption 102 - AES-128 segment encryption
91 - CEA-608 captions are automatically translated into standard HTML5 103 - CEA-608 captions are automatically translated into standard HTML5
92 [caption text 104 [caption text tracks][0]
93 tracks](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)
94 - Timed ID3 Metadata is automatically translated into HTML5 metedata 105 - Timed ID3 Metadata is automatically translated into HTML5 metedata
95 text tracks 106 text tracks
96 - Highly customizable adaptive bitrate selection 107 - Highly customizable adaptive bitrate selection
...@@ -98,6 +109,10 @@ are some highlights: ...@@ -98,6 +109,10 @@ are some highlights:
98 - Cross-domain credentials support with CORS 109 - Cross-domain credentials support with CORS
99 - Tight integration with video.js and a philosophy of exposing as much 110 - Tight integration with video.js and a philosophy of exposing as much
100 as possible with standard HTML APIs 111 as possible with standard HTML APIs
112 - Stream with multiple audio tracks and switching to those audio tracks
113 (see the docs folder) for info
114
115 [0]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track
101 116
102 ### Options 117 ### Options
103 118
...@@ -106,11 +121,22 @@ initialization. You can pass in options just like you would for other ...@@ -106,11 +121,22 @@ initialization. You can pass in options just like you would for other
106 parts of video.js: 121 parts of video.js:
107 122
108 ```javascript 123 ```javascript
109 videojs(video, { 124 // html5 for html hls
125 videojs(video, {html5: {
110 hls: { 126 hls: {
111 withCredentials: true 127 withCredentials: true
112 } 128 }
113 }); 129 }});
130
131 // or
132
133 // flash for flash hls
134 videojs(video, {flash: {
135 hls: {
136 withCredentials: true
137 }
138 }});
139
114 ``` 140 ```
115 141
116 #### withCredentials 142 #### withCredentials
...@@ -289,26 +315,8 @@ and most CDNs should have no trouble turning CORS on for your account. ...@@ -289,26 +315,8 @@ and most CDNs should have no trouble turning CORS on for your account.
289 315
290 ### Testing 316 ### Testing
291 317
292 For testing, you can either run `npm test` or use `grunt` directly. 318 For testing, you run `npm run test`. This will run tests using any of the
293 If you use `npm test`, it will only run the karma and end-to-end tests using chrome. 319 browsers that karma-detect-browsers detects on your machine.
294 You can specify which browsers you want the tests to run via grunt's `test` task.
295 You can use either grunt-style arguments or comma separated arguments:
296 ```
297 grunt test:chrome:firefox # grunt-style
298 grunt test:chrome,firefox # comma-separated
299 ```
300 Possible options are:
301 * `chromecanary`
302 * `phantomjs`
303 * `opera`
304 * `chrome`<sup>1</sup>
305 * `safari`<sup>1, 2</sup>
306 * `firefox`<sup>1</sup>
307 * `ie`<sup>1</sup>
308
309
310 _<sup>1</sup>supported end-to-end browsers_<br />
311 _<sup>2</sup>requires the [SafariDriver extension]( https://code.google.com/p/selenium/wiki/SafariDriver) to be installed_
312 320
313 ## Release History 321 ## Release History
314 Check out the [changelog](CHANGELOG.md) for a summary of each release. 322 Check out the [changelog](CHANGELOG.md) for a summary of each release.
......
1 # Multiple Alternative Audio Tracks
2 ## General
3 m3u8 manifests with multiple audio streams will have those streams added to `video.js` in an `AudioTrackList`. The `AudioTrackList` can be accessed using `player.audioTracks()` or `tech.audioTracks()`.
4
5 ## Mapping m3u8 metadata to AudioTracks
6 The mapping between `AudioTrack` and the parsed m3u8 file is fairly straight forward. The table below shows the mapping
7
8 | m3u8 | AudioTrack |
9 |---------|------------|
10 | label | label |
11 | lang | language |
12 | default | enabled |
13 | ??? | kind |
14 | ??? | id |
15
16 As you can see m3u8's do not have a property for `AudioTrack.id`, which means that we let `video.js` randomly generate the id for `AudioTrack`s. This will have no real impact on any part of the system as we do not use the `id` anywhere.
17
18 The other property that does not have a mapping in the m3u8 is `AudioTrack.kind`. It was decided that we would set the `kind` to `main` when `default` is set to `true` and in all other cases we set it to `alternative`
19
20 Below is a basic example of a mapping
21 m3u8 layout
22 ``` JavaScript
23 {
24 'media-group-1': [{
25 'audio-track-1': {
26 default: true,
27 lang: 'eng'
28 },
29 'audio-track-2': {
30 default: true,
31 lang: 'fr'
32 }
33 }]
34 }
35 ```
36
37 Corresponding AudioTrackList when media-group-1 is used (before any tracks have been changed)
38 ``` JavaScript
39 [{
40 label: 'audio-tracks-1',
41 enabled: true,
42 language: 'eng',
43 kind: 'main',
44 id: 'random'
45 }, {
46 label: 'audio-tracks-2',
47 enabled: false,
48 language: 'fr',
49 kind: 'alternative',
50 id: 'random'
51 }]
52 ```
53
54 ## Startup (how tracks are added and used)
55 > AudioTrack & AudioTrackList live in video.js
56
57 1. `HLS` creates a `MasterPlaylistController` and watches for the `loadedmetadata` event
58 1. `HLS` parses the m3u8 using the `MasterPlaylistController`
59 1. `MasterPlaylistController` creates a `PlaylistLoader` for the master m3u8
60 1. `MasterPlaylistController` creates `PlaylistLoader`s for every audio playlist
61 1. `MasterPlaylistController` creates a `SegmentLoader` for the main m3u8
62 1. `MasterPlaylistController` creates a `SegmentLoader` for a potential audio playlist
63 1. `HLS` sees the `loadedmetadata` and finds the currently selected MediaGroup and all the metadata
64 1. `HLS` removes all `AudioTrack`s from the `AudioTrackList`
65 1. `HLS` created `AudioTrack`s for the MediaGroup and adds them to the `AudioTrackList`
66 1. `HLS` calls `MasterPlaylistController`s `useAudio` with no arguments (causes it to use the currently enabled audio)
67 1. `MasterPlaylistController` turns off the current audio `PlaylistLoader` if it is on
68 1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
69 1. `MasterPlaylistController` turns on that `PlaylistLoader` and the Corresponding `SegmentLoader` (master or audio only)
70 1. `MediaSource`/`mux.js` determine how to mux
71
72 ## How tracks are switched
73 > AudioTrack & AudioTrackList live in video.js
74
75 1. `HLS` is setup to watch for the `changed` event on the `AudioTrackList`
76 1. User selects a new `AudioTrack` from a menu (where only one track can be enabled)
77 1. `AudioTrackList` enables the new `Audiotrack` and disables all others
78 1. `AudioTrackList` triggers a `changed` event
79 1. `HLS` sees the `changed` event and finds the newly enabled `AudioTrack`
80 1. `HLS` sends the `label` for the new `AudioTrack` to `MasterPlaylistController`s `useAudio` function
81 1. `MasterPlaylistController` turns off the current audio `PlaylistLoader` if it is on
82 1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
83 1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
84 1. `MasterPlaylistController` turns on that `PlaylistLoader` and the Corresponding `SegmentLoader` (master or audio only)
85 1. `MediaSource`/`mux.js` determine how to mux
86
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="utf-8" />
5 <title>Index</title>
6 </head>
7 <body>
8 <ul>
9 <li><a href="multiple-alternative-audio-tracks">Multiple Alternative Audio Tracks</a></li>
10 </ul>
11 </body>
12 </html>
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <title>Multiple Alternative Audio Tracks - Example</title>
6 <link href="/node_modules/video.js/dist/video-js.css" rel="stylesheet">
7 </head>
8 <body>
9 <h1>Multiple Alternative Audio Tracks</h1>
10 <p>Check the source of this page and the console for detailed information on this example</p>
11 <video id="maat-player" class="video-js vjs-default-skin" controls>
12 <source src="https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8" type="application/x-mpegURL">
13 </video>
14 <div id="audioTrackChoice">
15 <select id="enabled-audio-track" name="enabled-audio-track">
16 </select>
17 </div>
18 <script src="/node_modules/video.js/dist/video.js"></script>
19 <script src="/dist/videojs-contrib-hls.js"></script>
20 <script>
21 (function(window, videojs) {
22 var player = window.player = videojs('maat-player');
23 var audioTrackList = player.audioTracks();
24 var audioTrackSelect = document.getElementById("enabled-audio-track");
25 // watch for a change on the select element
26 // then change the enabled audio track
27 // only one can be enabled at a time, but video.js will
28 // handle that for us, all we need to do is enable the new
29 // track
30 audioTrackSelect.addEventListener('change', function() {
31 var track = audioTrackList[this.selectedIndex];
32 console.log('User switched to track ' + track.label);
33 track.enabled = true;
34 });
35
36 // watch for changes that will be triggered by any change
37 // to enabled on any audio track. Manually or through the
38 // select element
39 audioTrackList.on('change', function() {
40 for (var i = 0; i < audioTrackList.length; i++) {
41 var track = audioTrackList[i];
42 if (track.enabled) {
43 console.log('A new ' + track.label + ' has been enabled!');
44 }
45 }
46 });
47
48 // will be fired twice in this example
49 audioTrackList.on('addtrack', function() {
50 console.log('a track has been added to the audio track list');
51 });
52
53 // will not be fired at all unless you call
54 // audioTrackList.removeTrack(trackObj)
55 // we typically will not need to do this unless we have to load
56 // another video for some reason
57 audioTrackList.on('removetrack', function() {
58 console.log('a track has been removed from the audio track list');
59 });
60
61 // getting all the possible audio tracks from the track list
62 // get all of thier properties
63 // add each track to the select on the page
64 // this is all filled out by HLS when it parses the m3u8
65 player.on('loadeddata', function() {
66 console.log('There are ' + audioTrackList.length + ' audio tracks');
67 for (var i = 0; i < audioTrackList.length; i++) {
68 var track = audioTrackList[i];
69 var option = document.createElement("option");
70 option.text = track.label;
71 if (track.enabled) {
72 option.selected = true;
73 }
74 audioTrackSelect.add(option, i);
75 console.log('Track ' + (i + 1));
76 ['label', 'enabled', 'language', 'id', 'kind'].forEach(function(prop) {
77 console.log(" " + prop + ": " + track[prop]);
78 });
79 }
80 });
81 }(window, window.videojs));
82 </script>
83 </body>
84 </html>
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
43 <ul> 43 <ul>
44 <li><a href="/test/">Run unit tests in browser.</a></li> 44 <li><a href="/test/">Run unit tests in browser.</a></li>
45 <li><a href="/docs/api/">Read generated docs.</a></li> 45 <li><a href="/docs/api/">Read generated docs.</a></li>
46 <li><a href="/examples">Browse Examples</a></li>
46 </ul> 47 </ul>
47 48
48 <script src="/node_modules/video.js/dist/video.js"></script> 49 <script src="/node_modules/video.js/dist/video.js"></script>
...@@ -50,6 +51,7 @@ ...@@ -50,6 +51,7 @@
50 <script> 51 <script>
51 (function(window, videojs) { 52 (function(window, videojs) {
52 var player = window.player = videojs('videojs-contrib-hls-player'); 53 var player = window.player = videojs('videojs-contrib-hls-player');
54
53 // hook up the video switcher 55 // hook up the video switcher
54 var loadUrl = document.getElementById('load-url'); 56 var loadUrl = document.getElementById('load-url');
55 var url = document.getElementById('url'); 57 var url = document.getElementById('url');
......
...@@ -28,8 +28,7 @@ ...@@ -28,8 +28,7 @@
28 "docs:api": "jsdoc src -r -d docs/api", 28 "docs:api": "jsdoc src -r -d docs/api",
29 "docs:toc": "doctoc README.md", 29 "docs:toc": "doctoc README.md",
30 "lint": "vjsstandard", 30 "lint": "vjsstandard",
31 "prestart": "npm-run-all docs build", 31 "start": "npm-run-all -p watch start:*",
32 "start": "npm-run-all -p start:* watch:*",
33 "start:serve": "babel-node scripts/server.js", 32 "start:serve": "babel-node scripts/server.js",
34 "pretest": "npm-run-all lint build", 33 "pretest": "npm-run-all lint build",
35 "test": "karma start test/karma/detected.js", 34 "test": "karma start test/karma/detected.js",
...@@ -40,7 +39,10 @@ ...@@ -40,7 +39,10 @@
40 "preversion": "npm test", 39 "preversion": "npm test",
41 "version": "npm run build", 40 "version": "npm run build",
42 "watch": "npm-run-all -p watch:*", 41 "watch": "npm-run-all -p watch:*",
43 "watch:js": "watchify src/videojs-contrib-hls.js -t babelify -v -o dist/videojs-contrib-hls.js", 42 "watch:docs": "nodemon --watch src/ --exec npm run docs",
43 "watch:js": "npm-run-all -p watch:js:babel watch:js:browserify",
44 "watch:js:babel": "npm run build:js:babel -- --watch",
45 "watch:js:browserify": "watchify . -v -o dist/videojs-contrib-hls.js",
44 "watch:test": "npm-run-all -p watch:test:*", 46 "watch:test": "npm-run-all -p watch:test:*",
45 "watch:test:js": "node scripts/watch-test.js", 47 "watch:test:js": "node scripts/watch-test.js",
46 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", 48 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
...@@ -73,21 +75,19 @@ ...@@ -73,21 +75,19 @@
73 }, 75 },
74 "files": [ 76 "files": [
75 "CONTRIBUTING.md", 77 "CONTRIBUTING.md",
76 "dist-test/",
77 "dist/", 78 "dist/",
78 "docs/", 79 "docs/",
79 "es5/", 80 "es5/",
80 "index.html", 81 "index.html",
81 "scripts/", 82 "scripts/",
82 "src/", 83 "src/",
83 "test/", 84 "test/"
84 "utils/"
85 ], 85 ],
86 "dependencies": { 86 "dependencies": {
87 "pkcs7": "^0.2.2", 87 "pkcs7": "^0.2.2",
88 "video.js": "^5.2.1", 88 "video.js": "^5.10.1",
89 "videojs-contrib-media-sources": "^3.0.0", 89 "videojs-contrib-media-sources": "^3.1.0",
90 "videojs-swf": "^5.0.0" 90 "videojs-swf": "^5.0.2"
91 }, 91 },
92 "devDependencies": { 92 "devDependencies": {
93 "babel": "^5.8.0", 93 "babel": "^5.8.0",
...@@ -111,6 +111,7 @@ ...@@ -111,6 +111,7 @@
111 "karma-safari-launcher": "^0.1.0", 111 "karma-safari-launcher": "^0.1.0",
112 "lodash-compat": "^3.10.0", 112 "lodash-compat": "^3.10.0",
113 "minimist": "^1.2.0", 113 "minimist": "^1.2.0",
114 "nodemon": "^1.9.1",
114 "npm-run-all": "^1.2.0", 115 "npm-run-all": "^1.2.0",
115 "portscanner": "^1.0.0", 116 "portscanner": "^1.0.0",
116 "qunitjs": "^1.18.0", 117 "qunitjs": "^1.18.0",
......
...@@ -17,7 +17,7 @@ glob('test/**/*.test.js', function(err, files) { ...@@ -17,7 +17,7 @@ glob('test/**/*.test.js', function(err, files) {
17 }; 17 };
18 18
19 b.on('log', function(msg) { 19 b.on('log', function(msg) {
20 process.stdout.write(msg + '\n'); 20 process.stdout.write(msg + ' dist-test/videojs-contrib-hls.js\n');
21 }); 21 });
22 22
23 b.on('update', bundle); 23 b.on('update', bundle);
......
1 /**
2 * @file bin-utils.js
3 */
4
5 /**
6 * convert a TimeRange to text
7 *
8 * @param {TimeRange} range the timerange to use for conversion
9 * @param {Number} i the iterator on the range to convert
10 */
1 const textRange = function(range, i) { 11 const textRange = function(range, i) {
2 return range.start(i) + '-' + range.end(i); 12 return range.start(i) + '-' + range.end(i);
3 }; 13 };
4 14
15 /**
16 * format a number as hex string
17 *
18 * @param {Number} e The number
19 * @param {Number} i the iterator
20 */
5 const formatHexString = function(e, i) { 21 const formatHexString = function(e, i) {
6 let value = e.toString(16); 22 let value = e.toString(16);
7 23
...@@ -14,6 +30,9 @@ const formatAsciiString = function(e) { ...@@ -14,6 +30,9 @@ const formatAsciiString = function(e) {
14 return '.'; 30 return '.';
15 }; 31 };
16 32
33 /**
34 * utils to help dump binary data to the console
35 */
17 const utils = { 36 const utils = {
18 hexDump(data) { 37 hexDump(data) {
19 let bytes = Array.prototype.slice.call(data); 38 let bytes = Array.prototype.slice.call(data);
......
1 /* 1 /**
2 * aes.js 2 * @file decrypter/aes.js
3 * 3 *
4 * This file contains an adaptation of the AES decryption algorithm 4 * This file contains an adaptation of the AES decryption algorithm
5 * from the Standford Javascript Cryptography Library. That work is 5 * from the Standford Javascript Cryptography Library. That work is
...@@ -96,7 +96,7 @@ let aesTables = null; ...@@ -96,7 +96,7 @@ let aesTables = null;
96 * Schedule out an AES key for both encryption and decryption. This 96 * Schedule out an AES key for both encryption and decryption. This
97 * is a low-level class. Use a cipher mode to do bulk encryption. 97 * is a low-level class. Use a cipher mode to do bulk encryption.
98 * 98 *
99 * @constructor 99 * @class AES
100 * @param key {Array} The key as an array of 4, 6 or 8 words. 100 * @param key {Array} The key as an array of 4, 6 or 8 words.
101 */ 101 */
102 export default class AES { 102 export default class AES {
...@@ -184,13 +184,14 @@ export default class AES { ...@@ -184,13 +184,14 @@ export default class AES {
184 184
185 /** 185 /**
186 * Decrypt 16 bytes, specified as four 32-bit words. 186 * Decrypt 16 bytes, specified as four 32-bit words.
187 * @param encrypted0 {number} the first word to decrypt 187 *
188 * @param encrypted1 {number} the second word to decrypt 188 * @param {Number} encrypted0 the first word to decrypt
189 * @param encrypted2 {number} the third word to decrypt 189 * @param {Number} encrypted1 the second word to decrypt
190 * @param encrypted3 {number} the fourth word to decrypt 190 * @param {Number} encrypted2 the third word to decrypt
191 * @param out {Int32Array} the array to write the decrypted words 191 * @param {Number} encrypted3 the fourth word to decrypt
192 * @param {Int32Array} out the array to write the decrypted words
192 * into 193 * into
193 * @param offset {number} the offset into the output array to start 194 * @param {Number} offset the offset into the output array to start
194 * writing results 195 * writing results
195 * @return {Array} The plaintext. 196 * @return {Array} The plaintext.
196 */ 197 */
......
1 /**
2 * @file decrypter/async-stream.js
3 */
1 import Stream from '../stream'; 4 import Stream from '../stream';
2 5
3 /** 6 /**
4 * A wrapper around the Stream class to use setTiemout 7 * A wrapper around the Stream class to use setTiemout
5 * and run stream "jobs" Asynchronously 8 * and run stream "jobs" Asynchronously
9 *
10 * @class AsyncStream
11 * @extends Stream
6 */ 12 */
7 export default class AsyncStream extends Stream { 13 export default class AsyncStream extends Stream {
8 constructor() { 14 constructor() {
...@@ -11,6 +17,12 @@ export default class AsyncStream extends Stream { ...@@ -11,6 +17,12 @@ export default class AsyncStream extends Stream {
11 this.delay = 1; 17 this.delay = 1;
12 this.timeout_ = null; 18 this.timeout_ = null;
13 } 19 }
20
21 /**
22 * process an async job
23 *
24 * @private
25 */
14 processJob_() { 26 processJob_() {
15 this.jobs.shift()(); 27 this.jobs.shift()();
16 if (this.jobs.length) { 28 if (this.jobs.length) {
...@@ -20,6 +32,12 @@ export default class AsyncStream extends Stream { ...@@ -20,6 +32,12 @@ export default class AsyncStream extends Stream {
20 this.timeout_ = null; 32 this.timeout_ = null;
21 } 33 }
22 } 34 }
35
36 /**
37 * push a job into the stream
38 *
39 * @param {Function} job the job to push into the stream
40 */
23 push(job) { 41 push(job) {
24 this.jobs.push(job); 42 this.jobs.push(job);
25 if (!this.timeout_) { 43 if (!this.timeout_) {
......
1 /* 1 /**
2 * decrypter.js 2 * @file decrypter/decrypter.js
3 * 3 *
4 * An asynchronous implementation of AES-128 CBC decryption with 4 * An asynchronous implementation of AES-128 CBC decryption with
5 * PKCS#7 padding. 5 * PKCS#7 padding.
...@@ -20,12 +20,12 @@ const ntoh = function(word) { ...@@ -20,12 +20,12 @@ const ntoh = function(word) {
20 (word >>> 24); 20 (word >>> 24);
21 }; 21 };
22 22
23 /* eslint-disable max-len */
24 /** 23 /**
25 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 24 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding.
26 * @param encrypted {Uint8Array} the encrypted bytes 25 *
27 * @param key {Uint32Array} the bytes of the decryption key 26 * @param {Uint8Array} encrypted the encrypted bytes
28 * @param initVector {Uint32Array} the initialization vector (IV) to 27 * @param {Uint32Array} key the bytes of the decryption key
28 * @param {Uint32Array} initVector the initialization vector (IV) to
29 * use for the first round of CBC. 29 * use for the first round of CBC.
30 * @return {Uint8Array} the decrypted bytes 30 * @return {Uint8Array} the decrypted bytes
31 * 31 *
...@@ -33,7 +33,6 @@ const ntoh = function(word) { ...@@ -33,7 +33,6 @@ const ntoh = function(word) {
33 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 33 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
34 * @see https://tools.ietf.org/html/rfc2315 34 * @see https://tools.ietf.org/html/rfc2315
35 */ 35 */
36 /* eslint-enable max-len */
37 export const decrypt = function(encrypted, key, initVector) { 36 export const decrypt = function(encrypted, key, initVector) {
38 // word-level access to the encrypted bytes 37 // word-level access to the encrypted bytes
39 let encrypted32 = new Int32Array(encrypted.buffer, 38 let encrypted32 = new Int32Array(encrypted.buffer,
...@@ -106,6 +105,12 @@ export const decrypt = function(encrypted, key, initVector) { ...@@ -106,6 +105,12 @@ export const decrypt = function(encrypted, key, initVector) {
106 * The `Decrypter` class that manages decryption of AES 105 * The `Decrypter` class that manages decryption of AES
107 * data through `AsyncStream` objects and the `decrypt` 106 * data through `AsyncStream` objects and the `decrypt`
108 * function 107 * function
108 *
109 * @param {Uint8Array} encrypted the encrypted bytes
110 * @param {Uint32Array} key the bytes of the decryption key
111 * @param {Uint32Array} initVector the initialization vector (IV) to
112 * @param {Function} done the function to run when done
113 * @class Decrypter
109 */ 114 */
110 export class Decrypter { 115 export class Decrypter {
111 constructor(encrypted, key, initVector, done) { 116 constructor(encrypted, key, initVector, done) {
...@@ -137,6 +142,20 @@ export class Decrypter { ...@@ -137,6 +142,20 @@ export class Decrypter {
137 done(null, unpad(decrypted)); 142 done(null, unpad(decrypted));
138 }); 143 });
139 } 144 }
145
146 /**
147 * a getter for step the maximum number of bytes to process at one time
148 *
149 * @return {Number} the value of step 32000
150 */
151 static get STEP() {
152 // 4 * 8000;
153 return 32000;
154 }
155
156 /**
157 * @private
158 */
140 decryptChunk_(encrypted, key, initVector, decrypted) { 159 decryptChunk_(encrypted, key, initVector, decrypted) {
141 return function() { 160 return function() {
142 let bytes = decrypt(encrypted, key, initVector); 161 let bytes = decrypt(encrypted, key, initVector);
...@@ -146,10 +165,6 @@ export class Decrypter { ...@@ -146,10 +165,6 @@ export class Decrypter {
146 } 165 }
147 } 166 }
148 167
149 // the maximum number of bytes to process at one time
150 // 4 * 8000;
151 Decrypter.STEP = 32000;
152
153 export default { 168 export default {
154 Decrypter, 169 Decrypter,
155 decrypt 170 decrypt
......
1 /* 1 /**
2 * index.js 2 * @file decrypter/index.js
3 * 3 *
4 * Index module to easily import the primary components of AES-128 4 * Index module to easily import the primary components of AES-128
5 * decryption. Like this: 5 * decryption. Like this:
......
1 /**
2 * @file hls-audio-track.js
3 */
4 import {AudioTrack} from 'video.js';
5 import PlaylistLoader from './playlist-loader';
6
7 /**
8 * HlsAudioTrack extends video.js audio tracks but adds HLS
9 * specific data storage such as playlist loaders, mediaGroups
10 * and default/autoselect
11 *
12 * @param {Object} options options to create HlsAudioTrack with
13 * @class HlsAudioTrack
14 * @extends AudioTrack
15 */
16 export default class HlsAudioTrack extends AudioTrack {
17 constructor(options) {
18 super({
19 kind: options.default ? 'main' : 'alternative',
20 enabled: options.default || false,
21 language: options.language,
22 label: options.label
23 });
24
25 this.hls = options.hls;
26 this.autoselect = options.autoselect || false;
27 this.default = options.default || false;
28 this.withCredentials = options.withCredentials || false;
29 this.mediaGroups_ = [];
30 this.addLoader(options.mediaGroup, options.resolvedUri);
31 }
32
33 /**
34 * get a PlaylistLoader from this track given a mediaGroup name
35 *
36 * @param {String} mediaGroup the mediaGroup to get the loader for
37 * @return {PlaylistLoader|Null} the PlaylistLoader or null
38 */
39 getLoader(mediaGroup) {
40 for (let i = 0; i < this.mediaGroups_.length; i++) {
41 let mgl = this.mediaGroups_[i];
42
43 if (mgl.mediaGroup === mediaGroup) {
44 return mgl.loader;
45 }
46 }
47 }
48
49 /**
50 * add a PlaylistLoader given a mediaGroup, and a uri. for a combined track
51 * we store null for the playlistloader
52 *
53 * @param {String} mediaGroup the mediaGroup to get the loader for
54 * @param {String} uri the uri to get the audio track/mediaGroup from
55 */
56 addLoader(mediaGroup, uri = null) {
57 let loader = null;
58
59 if (uri) {
60 // TODO: this should probably happen upstream in Master Playlist
61 // Controller when we can switch PlaylistLoader sources
62 // then we can just store the uri here instead
63 loader = new PlaylistLoader(uri, this.hls, this.withCredentials);
64 }
65 this.mediaGroups_.push({mediaGroup, loader});
66 }
67
68 /**
69 * remove a playlist loader from a track given the mediaGroup
70 *
71 * @param {String} mediaGroup the mediaGroup to remove
72 */
73 removeLoader(mediaGroup) {
74 for (let i = 0; i < this.mediaGroups_.length; i++) {
75 let mgl = this.mediaGroups_[i];
76
77 if (mgl.mediaGroup === mediaGroup) {
78 if (mgl.loader) {
79 mgl.loader.dispose();
80 }
81 this.mediaGroups_.splice(i, 1);
82 return;
83 }
84 }
85 }
86
87 /**
88 * Dispose of this audio track and
89 * the playlist loader that it holds inside
90 */
91 dispose() {
92 let i = this.mediaGroups_.length;
93
94 while (i--) {
95 this.removeLoader(this.mediaGroups_[i].mediaGroup);
96 }
97 }
98 }
1 /** 1 /**
2 * @file m3u8/index.js
3 *
2 * Utilities for parsing M3U8 files. If the entire manifest is available, 4 * Utilities for parsing M3U8 files. If the entire manifest is available,
3 * `Parser` will create an object representation with enough detail for managing 5 * `Parser` will create an object representation with enough detail for managing
4 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives 6 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives
......
1 /**
2 * @file m3u8/line-stream.js
3 */
1 import Stream from '../stream'; 4 import Stream from '../stream';
5
2 /** 6 /**
3 * A stream that buffers string input and generates a `data` event for each 7 * A stream that buffers string input and generates a `data` event for each
4 * line. 8 * line.
9 *
10 * @class LineStream
11 * @extends Stream
5 */ 12 */
6 export default class LineStream extends Stream { 13 export default class LineStream extends Stream {
7 constructor() { 14 constructor() {
...@@ -11,7 +18,8 @@ export default class LineStream extends Stream { ...@@ -11,7 +18,8 @@ export default class LineStream extends Stream {
11 18
12 /** 19 /**
13 * Add new data to be parsed. 20 * Add new data to be parsed.
14 * @param data {string} the text to process 21 *
22 * @param {String} data the text to process
15 */ 23 */
16 push(data) { 24 push(data) {
17 let nextNewline; 25 let nextNewline;
......
1 /**
2 * @file m3u8/parse-stream.js
3 */
1 import Stream from '../stream'; 4 import Stream from '../stream';
2 5
3 // "forgiving" attribute list psuedo-grammar: 6 /**
4 // attributes -> keyvalue (',' keyvalue)* 7 * "forgiving" attribute list psuedo-grammar:
5 // keyvalue -> key '=' value 8 * attributes -> keyvalue (',' keyvalue)*
6 // key -> [^=]* 9 * keyvalue -> key '=' value
7 // value -> '"' [^"]* '"' | [^,]* 10 * key -> [^=]*
11 * value -> '"' [^"]* '"' | [^,]*
12 */
8 const attributeSeparator = function() { 13 const attributeSeparator = function() {
9 let key = '[^=]*'; 14 let key = '[^=]*';
10 let value = '"[^"]*"|[^,]*'; 15 let value = '"[^"]*"|[^,]*';
...@@ -13,6 +18,11 @@ const attributeSeparator = function() { ...@@ -13,6 +18,11 @@ const attributeSeparator = function() {
13 return new RegExp('(?:^|,)(' + keyvalue + ')'); 18 return new RegExp('(?:^|,)(' + keyvalue + ')');
14 }; 19 };
15 20
21 /**
22 * Parse attributes from a line given the seperator
23 *
24 * @param {String} attributes the attibute line to parse
25 */
16 const parseAttributes = function(attributes) { 26 const parseAttributes = function(attributes) {
17 // split the string using attributes as the separator 27 // split the string using attributes as the separator
18 let attrs = attributes.split(attributeSeparator()); 28 let attrs = attributes.split(attributeSeparator());
...@@ -57,6 +67,9 @@ const parseAttributes = function(attributes) { ...@@ -57,6 +67,9 @@ const parseAttributes = function(attributes) {
57 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 67 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
58 * tags are given the tag type `unknown` and a single additional property 68 * tags are given the tag type `unknown` and a single additional property
59 * `data` with the remainder of the input. 69 * `data` with the remainder of the input.
70 *
71 * @class ParseStream
72 * @extends Stream
60 */ 73 */
61 export default class ParseStream extends Stream { 74 export default class ParseStream extends Stream {
62 constructor() { 75 constructor() {
...@@ -65,7 +78,8 @@ export default class ParseStream extends Stream { ...@@ -65,7 +78,8 @@ export default class ParseStream extends Stream {
65 78
66 /** 79 /**
67 * Parses an additional line of input. 80 * Parses an additional line of input.
68 * @param line {string} a single line of an M3U8 file to parse 81 *
82 * @param {String} line a single line of an M3U8 file to parse
69 */ 83 */
70 push(line) { 84 push(line) {
71 let match; 85 let match;
...@@ -254,6 +268,18 @@ export default class ParseStream extends Stream { ...@@ -254,6 +268,18 @@ export default class ParseStream extends Stream {
254 this.trigger('data', event); 268 this.trigger('data', event);
255 return; 269 return;
256 } 270 }
271 match = (/^#EXT-X-MEDIA:?(.*)$/).exec(line);
272 if (match) {
273 event = {
274 type: 'tag',
275 tagType: 'media'
276 };
277 if (match[1]) {
278 event.attributes = parseAttributes(match[1]);
279 }
280 this.trigger('data', event);
281 return;
282 }
257 match = (/^#EXT-X-ENDLIST/).exec(line); 283 match = (/^#EXT-X-ENDLIST/).exec(line);
258 if (match) { 284 if (match) {
259 this.trigger('data', { 285 this.trigger('data', {
......
1 /**
2 * @file m3u8/parser.js
3 */
1 import Stream from '../stream' ; 4 import Stream from '../stream' ;
2 import LineStream from './line-stream'; 5 import LineStream from './line-stream';
3 import ParseStream from './parse-stream'; 6 import ParseStream from './parse-stream';
...@@ -20,6 +23,9 @@ import {mergeOptions} from 'video.js'; ...@@ -20,6 +23,9 @@ import {mergeOptions} from 'video.js';
20 * underlying input is somewhat nonsensical. It emits `info` and `warning` 23 * underlying input is somewhat nonsensical. It emits `info` and `warning`
21 * events during the parse if it encounters input that seems invalid or 24 * events during the parse if it encounters input that seems invalid or
22 * requires some property of the manifest object to be defaulted. 25 * requires some property of the manifest object to be defaulted.
26 *
27 * @class Parser
28 * @extends Stream
23 */ 29 */
24 export default class Parser extends Stream { 30 export default class Parser extends Stream {
25 constructor() { 31 constructor() {
...@@ -34,6 +40,14 @@ export default class Parser extends Stream { ...@@ -34,6 +40,14 @@ export default class Parser extends Stream {
34 let currentUri = {}; 40 let currentUri = {};
35 let key; 41 let key;
36 let noop = function() {}; 42 let noop = function() {};
43 let defaultMediaGroups = {
44 'AUDIO': {},
45 'VIDEO': {},
46 'CLOSED-CAPTIONS': {},
47 'SUBTITLES': {}
48 };
49 // group segments into numbered timelines delineated by discontinuities
50 let currentTimeline = 0;
37 51
38 // the manifest is empty until the parse stream begins delivering data 52 // the manifest is empty until the parse stream begins delivering data
39 this.manifest = { 53 this.manifest = {
...@@ -43,6 +57,9 @@ export default class Parser extends Stream { ...@@ -43,6 +57,9 @@ export default class Parser extends Stream {
43 57
44 // update the manifest with the m3u8 entry from the parse stream 58 // update the manifest with the m3u8 entry from the parse stream
45 this.parseStream.on('data', function(entry) { 59 this.parseStream.on('data', function(entry) {
60 let mediaGroup;
61 let rendition;
62
46 ({ 63 ({
47 tag() { 64 tag() {
48 // switch based on the tag type 65 // switch based on the tag type
...@@ -96,7 +113,6 @@ export default class Parser extends Stream { ...@@ -96,7 +113,6 @@ export default class Parser extends Stream {
96 } 113 }
97 114
98 this.manifest.segments = uris; 115 this.manifest.segments = uris;
99
100 }, 116 },
101 key() { 117 key() {
102 if (!entry.attributes) { 118 if (!entry.attributes) {
...@@ -149,6 +165,7 @@ export default class Parser extends Stream { ...@@ -149,6 +165,7 @@ export default class Parser extends Stream {
149 return; 165 return;
150 } 166 }
151 this.manifest.discontinuitySequence = entry.number; 167 this.manifest.discontinuitySequence = entry.number;
168 currentTimeline = entry.number;
152 }, 169 },
153 'playlist-type'() { 170 'playlist-type'() {
154 if (!(/VOD|EVENT/).test(entry.playlistType)) { 171 if (!(/VOD|EVENT/).test(entry.playlistType)) {
...@@ -161,6 +178,8 @@ export default class Parser extends Stream { ...@@ -161,6 +178,8 @@ export default class Parser extends Stream {
161 }, 178 },
162 'stream-inf'() { 179 'stream-inf'() {
163 this.manifest.playlists = uris; 180 this.manifest.playlists = uris;
181 this.manifest.mediaGroups =
182 this.manifest.mediaGroups || defaultMediaGroups;
164 183
165 if (!entry.attributes) { 184 if (!entry.attributes) {
166 this.trigger('warn', { 185 this.trigger('warn', {
...@@ -175,7 +194,48 @@ export default class Parser extends Stream { ...@@ -175,7 +194,48 @@ export default class Parser extends Stream {
175 currentUri.attributes = mergeOptions(currentUri.attributes, 194 currentUri.attributes = mergeOptions(currentUri.attributes,
176 entry.attributes); 195 entry.attributes);
177 }, 196 },
197 media() {
198 this.manifest.mediaGroups =
199 this.manifest.mediaGroups || defaultMediaGroups;
200
201 if (!(entry.attributes &&
202 entry.attributes.TYPE &&
203 entry.attributes['GROUP-ID'] &&
204 entry.attributes.NAME)) {
205 this.trigger('warn', {
206 message: 'ignoring incomplete or missing media group'
207 });
208 return;
209 }
210
211 // find the media group, creating defaults as necessary
212 let mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
213
214 mediaGroupType[entry.attributes['GROUP-ID']] =
215 mediaGroupType[entry.attributes['GROUP-ID']] || {};
216 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']];
217
218 // collect the rendition metadata
219 rendition = {
220 default: (/yes/i).test(entry.attributes.DEFAULT)
221 };
222 if (rendition.default) {
223 rendition.autoselect = true;
224 } else {
225 rendition.autoselect = (/yes/i).test(entry.attributes.AUTOSELECT);
226 }
227 if (entry.attributes.LANGUAGE) {
228 rendition.language = entry.attributes.LANGUAGE;
229 }
230 if (entry.attributes.URI) {
231 rendition.uri = entry.attributes.URI;
232 }
233
234 // insert the new rendition
235 mediaGroup[entry.attributes.NAME] = rendition;
236 },
178 discontinuity() { 237 discontinuity() {
238 currentTimeline += 1;
179 currentUri.discontinuity = true; 239 currentUri.discontinuity = true;
180 this.manifest.discontinuityStarts.push(uris.length); 240 this.manifest.discontinuityStarts.push(uris.length);
181 }, 241 },
...@@ -215,6 +275,7 @@ export default class Parser extends Stream { ...@@ -215,6 +275,7 @@ export default class Parser extends Stream {
215 if (key) { 275 if (key) {
216 currentUri.key = key; 276 currentUri.key = key;
217 } 277 }
278 currentUri.timeline = currentTimeline;
218 279
219 // prepare for the next URI 280 // prepare for the next URI
220 currentUri = {}; 281 currentUri = {};
...@@ -229,7 +290,8 @@ export default class Parser extends Stream { ...@@ -229,7 +290,8 @@ export default class Parser extends Stream {
229 290
230 /** 291 /**
231 * Parse the input string and update the manifest object. 292 * Parse the input string and update the manifest object.
232 * @param chunk {string} a potentially incomplete portion of the manifest 293 *
294 * @param {String} chunk a potentially incomplete portion of the manifest
233 */ 295 */
234 push(chunk) { 296 push(chunk) {
235 this.lineStream.push(chunk); 297 this.lineStream.push(chunk);
...@@ -246,4 +308,3 @@ export default class Parser extends Stream { ...@@ -246,4 +308,3 @@ export default class Parser extends Stream {
246 } 308 }
247 309
248 } 310 }
249
......
1 /**
2 * @file master-playlist-controller.js
3 */
4 import PlaylistLoader from './playlist-loader';
5 import SegmentLoader from './segment-loader';
6 import Ranges from './ranges';
7 import videojs from 'video.js';
8 import HlsAudioTrack from './hls-audio-track';
9
10 // 5 minute blacklist
11 const BLACKLIST_DURATION = 5 * 60 * 1000;
12 let Hls;
13
14 const parseCodecs = function(codecs) {
15 let result = {
16 codecCount: 0,
17 videoCodec: null,
18 audioProfile: null
19 };
20
21 result.codecCount = codecs.split(',').length;
22 result.codecCount = result.codecCount || 2;
23
24 // parse the video codec but ignore the version
25 result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs);
26 result.videoCodec = result.videoCodec && result.videoCodec[2];
27
28 // parse the last field of the audio codec
29 result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs);
30 result.audioProfile = result.audioProfile && result.audioProfile[2];
31
32 return result;
33 };
34
35 /**
36 * the master playlist controller controller all interactons
37 * between playlists and segmentloaders. At this time this mainly
38 * involves a master playlist and a series of audio playlists
39 * if they are available
40 *
41 * @class MasterPlaylistController
42 * @extends videojs.EventTarget
43 */
44 export default class MasterPlaylistController extends videojs.EventTarget {
45 constructor({
46 url,
47 withCredentials,
48 mode,
49 tech,
50 bandwidth,
51 externHls
52 }) {
53 super();
54
55 Hls = externHls;
56
57 this.withCredentials = withCredentials;
58 this.tech_ = tech;
59 this.hls_ = tech.hls;
60 this.mode_ = mode;
61 this.audioTracks_ = [];
62
63 this.mediaSource = new videojs.MediaSource({ mode });
64 this.mediaSource.on('audioinfo', (e) => this.trigger(e));
65 // load the media source into the player
66 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
67
68 let segmentLoaderOptions = {
69 hls: this.hls_,
70 mediaSource: this.mediaSource,
71 currentTime: this.tech_.currentTime.bind(this.tech_),
72 withCredentials: this.withCredentials,
73 seekable: () => this.seekable(),
74 seeking: () => this.tech_.seeking(),
75 setCurrentTime: (a) => this.setCurrentTime(a),
76 hasPlayed: () => this.tech_.played().length !== 0,
77 bandwidth
78 };
79
80 // combined audio/video or just video when alternate audio track is selected
81 this.mainSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
82 // alternate audio track
83 this.audioSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
84
85 if (!url) {
86 throw new Error('A non-empty playlist URL is required');
87 }
88
89 this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials);
90
91 this.masterPlaylistLoader_.on('loadedmetadata', () => {
92 let media = this.masterPlaylistLoader_.media();
93
94 // if this isn't a live video and preload permits, start
95 // downloading segments
96 if (media.endList && this.tech_.preload() !== 'none') {
97 this.mainSegmentLoader_.playlist(media);
98 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_);
99 this.mainSegmentLoader_.load();
100 }
101
102 this.setupSourceBuffer_();
103 this.setupFirstPlay();
104 this.useAudio();
105 });
106
107 this.masterPlaylistLoader_.on('loadedplaylist', () => {
108 let updatedPlaylist = this.masterPlaylistLoader_.media();
109 let seekable;
110
111 if (!updatedPlaylist) {
112 // select the initial variant
113 this.initialMedia_ = this.selectPlaylist();
114 this.masterPlaylistLoader_.media(this.initialMedia_);
115 this.fillAudioTracks_();
116
117 this.trigger('selectedinitialmedia');
118 return;
119 }
120
121 this.mainSegmentLoader_.playlist(updatedPlaylist);
122 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_);
123 this.updateDuration();
124
125 // update seekable
126 seekable = this.seekable();
127 if (!updatedPlaylist.endList && seekable.length !== 0) {
128 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
129 }
130 });
131
132 this.masterPlaylistLoader_.on('error', () => {
133 this.blacklistCurrentPlaylist(this.masterPlaylistLoader_.error);
134 });
135
136 this.masterPlaylistLoader_.on('mediachanging', () => {
137 this.mainSegmentLoader_.pause();
138 });
139
140 this.masterPlaylistLoader_.on('mediachange', () => {
141 this.mainSegmentLoader_.abort();
142 this.mainSegmentLoader_.load();
143 this.tech_.trigger({
144 type: 'mediachange',
145 bubbles: true
146 });
147 });
148
149 this.mainSegmentLoader_.on('progress', () => {
150 // figure out what stream the next segment should be downloaded from
151 // with the updated bandwidth information
152 this.masterPlaylistLoader_.media(this.selectPlaylist());
153
154 this.trigger('progress');
155 });
156
157 this.mainSegmentLoader_.on('error', () => {
158 this.blacklistCurrentPlaylist(this.mainSegmentLoader_.error());
159 });
160
161 this.audioSegmentLoader_.on('error', () => {
162 videojs.log.warn('Problem encountered with the current alternate audio track' +
163 '. Switching back to default.');
164 this.audioSegmentLoader_.abort();
165 this.audioPlaylistLoader_ = null;
166 this.useAudio();
167 });
168
169 this.masterPlaylistLoader_.load();
170 }
171
172 /**
173 * fill our internal list of HlsAudioTracks with data from
174 * the master playlist or use a default
175 *
176 * @private
177 */
178 fillAudioTracks_() {
179 let master = this.master();
180 let mediaGroups = master.mediaGroups || {};
181
182 // force a default if we have none or we are not
183 // in html5 mode (the only mode to support more than one
184 // audio track)
185 if (!mediaGroups ||
186 !mediaGroups.AUDIO ||
187 Object.keys(mediaGroups.AUDIO).length === 0 ||
188 this.mode_ !== 'html5') {
189 // "main" audio group, track name "default"
190 mediaGroups = videojs.mergeOptions(mediaGroups, {AUDIO: {
191 main: {default: {default: true}}}
192 });
193 }
194
195 let tracks = {};
196
197 for (let mediaGroup in mediaGroups.AUDIO) {
198 for (let label in mediaGroups.AUDIO[mediaGroup]) {
199 let properties = mediaGroups.AUDIO[mediaGroup][label];
200
201 // if the track already exists add a new "location"
202 // since tracks in different mediaGroups are actually the same
203 // track with different locations to download them from
204 if (tracks[label]) {
205 tracks[label].addLoader(mediaGroup, properties.resolvedUri);
206 continue;
207 }
208
209 let track = new HlsAudioTrack(videojs.mergeOptions(properties, {
210 hls: this.hls_,
211 withCredentials: this.withCredential,
212 mediaGroup,
213 label
214 }));
215
216 tracks[label] = track;
217 this.audioTracks_.push(track);
218 }
219 }
220 }
221
222 /**
223 * Call load on our SegmentLoaders
224 */
225 load() {
226 this.mainSegmentLoader_.load();
227 if (this.audioPlaylistLoader_) {
228 this.audioSegmentLoader_.load();
229 }
230 }
231
232 /**
233 * Get the current active Media Group for Audio
234 * given the selected playlist and its attributes
235 */
236 activeAudioGroup() {
237 let media = this.masterPlaylistLoader_.media();
238 let mediaGroup = 'main';
239
240 if (media && media.attributes && media.attributes.AUDIO) {
241 mediaGroup = media.attributes.AUDIO;
242 }
243
244 return mediaGroup;
245 }
246
247 /**
248 * Use any audio track that we have, and start to load it
249 */
250 useAudio() {
251 let track;
252
253 this.audioTracks_.forEach((t) => {
254 if (!track && t.enabled) {
255 track = t;
256 }
257 });
258
259 // called too early or no track is enabled
260 if (!track) {
261 return;
262 }
263
264 // Pause any alternative audio
265 if (this.audioPlaylistLoader_) {
266 this.audioPlaylistLoader_.pause();
267 this.audioPlaylistLoader_ = null;
268 this.audioSegmentLoader_.pause();
269 }
270
271 // If the audio track for the active audio group has
272 // a playlist loader than it is an alterative audio track
273 // otherwise it is a part of the mainSegmenLoader
274 let loader = track.getLoader(this.activeAudioGroup());
275
276 if (!loader) {
277 this.mainSegmentLoader_.clearBuffer();
278 return;
279 }
280
281 // TODO: it may be better to create the playlist loader here
282 // when we can change an audioPlaylistLoaders src
283 this.audioPlaylistLoader_ = loader;
284
285 if (this.audioPlaylistLoader_.started) {
286 this.audioPlaylistLoader_.load();
287 this.audioSegmentLoader_.load();
288 this.audioSegmentLoader_.clearBuffer();
289 return;
290 }
291
292 this.audioPlaylistLoader_.on('loadedmetadata', () => {
293 /* eslint-disable no-shadow */
294 let media = this.audioPlaylistLoader_.media();
295 /* eslint-enable no-shadow */
296
297 this.audioSegmentLoader_.playlist(media);
298 this.addMimeType_(this.audioSegmentLoader_, 'mp4a.40.2', media);
299
300 // if the video is already playing, or if this isn't a live video and preload
301 // permits, start downloading segments
302 if (!this.tech_.paused() ||
303 (media.endList && this.tech_.preload() !== 'none')) {
304 this.audioSegmentLoader_.load();
305 }
306
307 if (!media.endList) {
308 // trigger the playlist loader to start "expired time"-tracking
309 this.audioPlaylistLoader_.trigger('firstplay');
310 }
311 });
312
313 this.audioPlaylistLoader_.on('loadedplaylist', () => {
314 let updatedPlaylist;
315
316 if (this.audioPlaylistLoader_) {
317 updatedPlaylist = this.audioPlaylistLoader_.media();
318 }
319
320 if (!updatedPlaylist) {
321 // only one playlist to select
322 this.audioPlaylistLoader_.media(
323 this.audioPlaylistLoader_.playlists.master.playlists[0]);
324 return;
325 }
326
327 this.audioSegmentLoader_.playlist(updatedPlaylist);
328 });
329
330 this.audioPlaylistLoader_.on('error', () => {
331 videojs.log.warn('Problem encountered loading the alternate audio track' +
332 '. Switching back to default.');
333 this.audioSegmentLoader_.abort();
334 this.audioPlaylistLoader_ = null;
335 this.useAudio();
336 });
337
338 this.audioSegmentLoader_.clearBuffer();
339 this.audioPlaylistLoader_.start();
340 }
341
342 /**
343 * Re-tune playback quality level for the current player
344 * conditions. This method may perform destructive actions, like
345 * removing already buffered content, to readjust the currently
346 * active playlist quickly.
347 *
348 * @private
349 */
350 fastQualityChange_() {
351 let media = this.selectPlaylist();
352
353 if (media !== this.masterPlaylistLoader_.media()) {
354 this.masterPlaylistLoader_.media(media);
355 this.mainSegmentLoader_.sourceUpdater_.remove(this.currentTimeFunc() + 5, Infinity);
356 }
357 }
358
359 /**
360 * Begin playback.
361 */
362 play() {
363 if (this.setupFirstPlay()) {
364 return;
365 }
366
367 if (this.tech_.ended()) {
368 this.tech_.setCurrentTime(0);
369 }
370
371 this.load();
372
373 // if the viewer has paused and we fell out of the live window,
374 // seek forward to the earliest available position
375 if (this.tech_.duration() === Infinity) {
376 if (this.tech_.currentTime() < this.tech_.seekable().start(0)) {
377 return this.tech_.setCurrentTime(this.tech_.seekable().start(0));
378 }
379 }
380
381 }
382
383 /**
384 * Seek to the latest media position if this is a live video and the
385 * player and video are loaded and initialized.
386 */
387 setupFirstPlay() {
388 let seekable;
389 let media = this.masterPlaylistLoader_.media();
390
391 // check that everything is ready to begin buffering
392 // 1) the active media playlist is available
393 if (media &&
394 // 2) the video is a live stream
395 !media.endList &&
396
397 // 3) the player is not paused
398 !this.tech_.paused() &&
399
400 // 4) the player has not started playing
401 !this.hasPlayed_) {
402
403 this.load();
404
405 // trigger the playlist loader to start "expired time"-tracking
406 this.masterPlaylistLoader_.trigger('firstplay');
407 this.hasPlayed_ = true;
408
409 // seek to the latest media position for live videos
410 seekable = this.seekable();
411 if (seekable.length) {
412 this.tech_.setCurrentTime(seekable.end(0));
413 }
414
415 return true;
416 }
417 return false;
418 }
419
420 /**
421 * handle the sourceopen event on the MediaSource
422 *
423 * @private
424 */
425 handleSourceOpen_() {
426 // Only attempt to create the source buffer if none already exist.
427 // handleSourceOpen is also called when we are "re-opening" a source buffer
428 // after `endOfStream` has been called (in response to a seek for instance)
429 this.setupSourceBuffer_();
430
431 // if autoplay is enabled, begin playback. This is duplicative of
432 // code in video.js but is required because play() must be invoked
433 // *after* the media source has opened.
434 if (this.tech_.autoplay()) {
435 this.tech_.play();
436 }
437
438 this.trigger('sourceopen');
439 }
440
441 /**
442 * Blacklists a playlist when an error occurs for a set amount of time
443 * making it unavailable for selection by the rendition selection algorithm
444 * and then forces a new playlist (rendition) selection.
445 *
446 * @param {Object=} error an optional error that may include the playlist
447 * to blacklist
448 */
449 blacklistCurrentPlaylist(error = {}) {
450 let currentPlaylist;
451 let nextPlaylist;
452
453 // If the `error` was generated by the playlist loader, it will contain
454 // the playlist we were trying to load (but failed) and that should be
455 // blacklisted instead of the currently selected playlist which is likely
456 // out-of-date in this scenario
457 currentPlaylist = error.playlist || this.masterPlaylistLoader_.media();
458
459 // If there is no current playlist, then an error occurred while we were
460 // trying to load the master OR while we were disposing of the tech
461 if (!currentPlaylist) {
462 this.error = error;
463 return this.mediaSource.endOfStream('network');
464 }
465
466 // Blacklist this playlist
467 currentPlaylist.excludeUntil = Date.now() + BLACKLIST_DURATION;
468
469 // Select a new playlist
470 nextPlaylist = this.selectPlaylist();
471
472 if (nextPlaylist) {
473 videojs.log.warn('Problem encountered with the current ' +
474 'HLS playlist. Switching to another playlist.');
475
476 return this.masterPlaylistLoader_.media(nextPlaylist);
477 }
478 videojs.log.warn('Problem encountered with the current ' +
479 'HLS playlist. No suitable alternatives found.');
480 // We have no more playlists we can select so we must fail
481 this.error = error;
482 return this.mediaSource.endOfStream('network');
483 }
484
485 /**
486 * Pause all segment loaders
487 */
488 pauseLoading() {
489 this.mainSegmentLoader_.pause();
490 if (this.audioPlaylistLoader_) {
491 this.audioSegmentLoader_.pause();
492 }
493 }
494
495 /**
496 * set the current time on all segment loaders
497 *
498 * @param {TimeRange} currentTime the current time to set
499 * @return {TimeRange} the current time
500 */
501 setCurrentTime(currentTime) {
502 let buffered = Ranges.findRange(this.tech_.buffered(), currentTime);
503
504 if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) {
505 // return immediately if the metadata is not ready yet
506 return 0;
507 }
508
509 // it's clearly an edge-case but don't thrown an error if asked to
510 // seek within an empty playlist
511 if (!this.masterPlaylistLoader_.media().segments) {
512 return 0;
513 }
514
515 // if the seek location is already buffered, continue buffering as
516 // usual
517 if (buffered && buffered.length) {
518 return currentTime;
519 }
520
521 // cancel outstanding requests so we begin buffering at the new
522 // location
523 this.mainSegmentLoader_.abort();
524 if (this.audioPlaylistLoader_) {
525 this.audioSegmentLoader_.abort();
526 }
527
528 if (!this.tech_.paused()) {
529 this.mainSegmentLoader_.load();
530 if (this.audioPlaylistLoader_) {
531 this.audioSegmentLoader_.load();
532 }
533 }
534 }
535
536 /**
537 * get the current duration
538 *
539 * @return {TimeRange} the duration
540 */
541 duration() {
542 if (!this.masterPlaylistLoader_) {
543 return 0;
544 }
545
546 if (this.mediaSource) {
547 return this.mediaSource.duration;
548 }
549
550 return Hls.Playlist.duration(this.masterPlaylistLoader_.media());
551 }
552
553 /**
554 * check the seekable range
555 *
556 * @return {TimeRange} the seekable range
557 */
558 seekable() {
559 let media;
560 let mainSeekable;
561 let audioSeekable;
562
563 if (!this.masterPlaylistLoader_) {
564 return videojs.createTimeRanges();
565 }
566 media = this.masterPlaylistLoader_.media();
567 if (!media) {
568 return videojs.createTimeRanges();
569 }
570
571 mainSeekable = Hls.Playlist.seekable(media,
572 this.masterPlaylistLoader_.expired_);
573 if (mainSeekable.length === 0) {
574 return mainSeekable;
575 }
576
577 if (this.audioPlaylistLoader_) {
578 audioSeekable = Hls.Playlist.seekable(this.audioPlaylistLoader_.media(),
579 this.audioPlaylistLoader_.expired_);
580 if (audioSeekable.length === 0) {
581 return audioSeekable;
582 }
583 }
584
585 if (!audioSeekable) {
586 // seekable has been calculated based on buffering video data so it
587 // can be returned directly
588 return mainSeekable;
589 }
590
591 return videojs.createTimeRanges([[
592 (audioSeekable.start(0) > mainSeekable.start(0)) ? audioSeekable.start(0) :
593 mainSeekable.start(0),
594 (audioSeekable.end(0) < mainSeekable.end(0)) ? audioSeekable.end(0) :
595 mainSeekable.end(0)
596 ]]);
597 }
598
599 /**
600 * Update the player duration
601 */
602 updateDuration() {
603 let oldDuration = this.mediaSource.duration;
604 let newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media());
605 let buffered = this.tech_.buffered();
606 let setDuration = () => {
607 this.mediaSource.duration = newDuration;
608 this.tech_.trigger('durationchange');
609
610 this.mediaSource.removeEventListener('sourceopen', setDuration);
611 };
612
613 if (buffered.length > 0) {
614 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
615 }
616
617 // if the duration has changed, invalidate the cached value
618 if (oldDuration !== newDuration) {
619 // update the duration
620 if (this.mediaSource.readyState !== 'open') {
621 this.mediaSource.addEventListener('sourceopen', setDuration);
622 } else {
623 setDuration();
624 }
625 }
626 }
627
628 /**
629 * dispose of the MasterPlaylistController and everything
630 * that it controls
631 */
632 dispose() {
633 this.masterPlaylistLoader_.dispose();
634 this.audioTracks_.forEach((track) => {
635 track.dispose();
636 });
637 this.audioTracks_.length = 0;
638 this.mainSegmentLoader_.dispose();
639 this.audioSegmentLoader_.dispose();
640 }
641
642 /**
643 * return the master playlist object if we have one
644 *
645 * @return {Object} the master playlist object that we parsed
646 */
647 master() {
648 return this.masterPlaylistLoader_.master;
649 }
650
651 /**
652 * return the currently selected playlist
653 *
654 * @return {Object} the currently selected playlist object that we parsed
655 */
656 media() {
657 // playlist loader will not return media if it has not been fully loaded
658 return this.masterPlaylistLoader_.media() || this.initialMedia_;
659 }
660
661 /**
662 * setup our internal source buffers on our segment Loaders
663 *
664 * @private
665 */
666 setupSourceBuffer_() {
667 let media = this.masterPlaylistLoader_.media();
668
669 // wait until a media playlist is available and the Media Source is
670 // attached
671 if (!media || this.mediaSource.readyState !== 'open') {
672 return;
673 }
674
675 this.addMimeType_(this.mainSegmentLoader_, 'avc1.4d400d, mp4a.40.2', media);
676
677 // exclude any incompatible variant streams from future playlist
678 // selection
679 this.excludeIncompatibleVariants_(media);
680 }
681
682 /**
683 * add a time type to a segmentLoader
684 *
685 * @param {SegmentLoader} segmentLoader the segmentloader to work on
686 * @param {String} codecs to use by default
687 * @param {Object} the parsed media object
688 * @private
689 */
690 addMimeType_(segmentLoader, defaultCodecs, media) {
691 let mimeType = 'video/mp2t';
692
693 // if the codecs were explicitly specified, pass them along to the
694 // source buffer
695 if (media.attributes && media.attributes.CODECS) {
696 mimeType += '; codecs="' + media.attributes.CODECS + '"';
697 } else {
698 mimeType += '; codecs="' + defaultCodecs + '"';
699 }
700 segmentLoader.mimeType(mimeType);
701 }
702
703 /**
704 * Blacklist playlists that are known to be codec or
705 * stream-incompatible with the SourceBuffer configuration. For
706 * instance, Media Source Extensions would cause the video element to
707 * stall waiting for video data if you switched from a variant with
708 * video and audio to an audio-only one.
709 *
710 * @param {Object} media a media playlist compatible with the current
711 * set of SourceBuffers. Variants in the current master playlist that
712 * do not appear to have compatible codec or stream configurations
713 * will be excluded from the default playlist selection algorithm
714 * indefinitely.
715 * @private
716 */
717 excludeIncompatibleVariants_(media) {
718 let master = this.masterPlaylistLoader_.master;
719 let codecCount = 2;
720 let videoCodec = null;
721 let audioProfile = null;
722 let codecs;
723
724 if (media.attributes && media.attributes.CODECS) {
725 codecs = parseCodecs(media.attributes.CODECS);
726 videoCodec = codecs.videoCodec;
727 audioProfile = codecs.audioProfile;
728 codecCount = codecs.codecCount;
729 }
730 master.playlists.forEach(function(variant) {
731 let variantCodecs = {
732 codecCount: 2,
733 videoCodec: null,
734 audioProfile: null
735 };
736
737 if (variant.attributes && variant.attributes.CODECS) {
738 variantCodecs = parseCodecs(variant.attributes.CODECS);
739 }
740
741 // if the streams differ in the presence or absence of audio or
742 // video, they are incompatible
743 if (variantCodecs.codecCount !== codecCount) {
744 variant.excludeUntil = Infinity;
745 }
746
747 // if h.264 is specified on the current playlist, some flavor of
748 // it must be specified on all compatible variants
749 if (variantCodecs.videoCodec !== videoCodec) {
750 variant.excludeUntil = Infinity;
751 }
752 // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
753 // AAC audio in Chrome 46. Don't mix the two.
754 if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
755 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
756 variant.excludeUntil = Infinity;
757 }
758 });
759 }
760 }
1 /** 1 /**
2 * playlist-loader 2 * @file playlist-loader.js
3 * 3 *
4 * A state machine that manages the loading, caching, and updating of 4 * A state machine that manages the loading, caching, and updating of
5 * M3U8 playlists. 5 * M3U8 playlists.
...@@ -11,21 +11,53 @@ import Stream from './stream'; ...@@ -11,21 +11,53 @@ import Stream from './stream';
11 import m3u8 from './m3u8'; 11 import m3u8 from './m3u8';
12 12
13 /** 13 /**
14 * Returns a new master playlist that is the result of merging an 14 * Returns a new array of segments that is the result of merging
15 * updated media playlist into the original version. If the 15 * properties from an older list of segments onto an updated
16 * updated media playlist does not match any of the playlist 16 * list. No properties on the updated playlist will be overridden.
17 * entries in the original master playlist, null is returned. 17 *
18 * @param master {object} a parsed master M3U8 object 18 * @param {Array} original the outdated list of segments
19 * @param media {object} a parsed media M3U8 object 19 * @param {Array} update the updated list of segments
20 * @return {object} a new object that represents the original 20 * @param {Number=} offset the index of the first update
21 * master playlist with the updated media playlist merged in, or 21 * segment in the original segment list. For non-live playlists,
22 * null if the merge produced no change. 22 * this should always be zero and does not need to be
23 */ 23 * specified. For live playlists, it should be the difference
24 * between the media sequence numbers in the original and updated
25 * playlists.
26 * @return a list of merged segment objects
27 */
28 const updateSegments = function(original, update, offset) {
29 let result = update.slice();
30 let length;
31 let i;
32
33 offset = offset || 0;
34 length = Math.min(original.length, update.length + offset);
35
36 for (i = offset; i < length; i++) {
37 result[i - offset] = mergeOptions(original[i], result[i - offset]);
38 }
39 return result;
40 };
41
42 /**
43 * Returns a new master playlist that is the result of merging an
44 * updated media playlist into the original version. If the
45 * updated media playlist does not match any of the playlist
46 * entries in the original master playlist, null is returned.
47 *
48 * @param {Object} master a parsed master M3U8 object
49 * @param {Object} media a parsed media M3U8 object
50 * @return {Object} a new object that represents the original
51 * master playlist with the updated media playlist merged in, or
52 * null if the merge produced no change.
53 */
24 const updateMaster = function(master, media) { 54 const updateMaster = function(master, media) {
25 let changed = false; 55 let changed = false;
26 let result = mergeOptions(master, {}); 56 let result = mergeOptions(master, {});
27 let i = master.playlists.length; 57 let i = master.playlists.length;
28 let playlist; 58 let playlist;
59 let segment;
60 let j;
29 61
30 while (i--) { 62 while (i--) {
31 playlist = result.playlists[i]; 63 playlist = result.playlists[i];
...@@ -45,10 +77,25 @@ const updateMaster = function(master, media) { ...@@ -45,10 +77,25 @@ const updateMaster = function(master, media) {
45 // if the update could overlap existing segment information, 77 // if the update could overlap existing segment information,
46 // merge the two lists 78 // merge the two lists
47 if (playlist.segments) { 79 if (playlist.segments) {
48 result.playlists[i].segments = updateSegments(playlist.segments, 80 result.playlists[i].segments = updateSegments(
49 media.segments, 81 playlist.segments,
50 media.mediaSequence - 82 media.segments,
51 playlist.mediaSequence); 83 media.mediaSequence - playlist.mediaSequence
84 );
85 }
86 // resolve any missing segment and key URIs
87 j = 0;
88 if (result.playlists[i].segments) {
89 j = result.playlists[i].segments.length;
90 }
91 while (j--) {
92 segment = result.playlists[i].segments[j];
93 if (!segment.resolvedUri) {
94 segment.resolvedUri = resolveUrl(playlist.resolvedUri, segment.uri);
95 }
96 if (segment.key && !segment.key.resolvedUri) {
97 segment.key.resolvedUri = resolveUrl(playlist.resolvedUri, segment.key.uri);
98 }
52 } 99 }
53 changed = true; 100 changed = true;
54 } 101 }
...@@ -57,258 +104,297 @@ const updateMaster = function(master, media) { ...@@ -57,258 +104,297 @@ const updateMaster = function(master, media) {
57 }; 104 };
58 105
59 /** 106 /**
60 * Returns a new array of segments that is the result of merging 107 * Load a playlist from a remote loacation
61 * properties from an older list of segments onto an updated 108 *
62 * list. No properties on the updated playlist will be overridden. 109 * @class PlaylistLoader
63 * @param original {array} the outdated list of segments 110 * @extends Stream
64 * @param update {array} the updated list of segments 111 * @param {String} srcUrl the url to start with
65 * @param offset {number} (optional) the index of the first update 112 * @param {Boolean} withCredentials the withCredentials xhr option
66 * segment in the original segment list. For non-live playlists, 113 * @constructor
67 * this should always be zero and does not need to be
68 * specified. For live playlists, it should be the difference
69 * between the media sequence numbers in the original and updated
70 * playlists.
71 * @return a list of merged segment objects
72 */ 114 */
73 const updateSegments = function(original, update, offset) { 115 const PlaylistLoader = function(srcUrl, hls, withCredentials) {
74 let result = update.slice(); 116 /* eslint-disable consistent-this */
75 let length; 117 let loader = this;
76 let i; 118 /* eslint-enable consistent-this */
119 let dispose;
120 let mediaUpdateTimeout;
121 let request;
122 let playlistRequestError;
123 let haveMetadata;
124
125 PlaylistLoader.prototype.constructor.call(this);
126
127 this.hls_ = hls;
128
129 // a flag that disables "expired time"-tracking this setting has
130 // no effect when not playing a live stream
131 this.trackExpiredTime_ = false;
132
133 if (!srcUrl) {
134 throw new Error('A non-empty playlist URL is required');
135 }
77 136
78 offset = offset || 0; 137 playlistRequestError = function(xhr, url, startingState) {
79 length = Math.min(original.length, update.length + offset); 138 loader.setBandwidth(request || xhr);
80 139
81 for (i = offset; i < length; i++) { 140 // any in-flight request is now finished
82 result[i - offset] = mergeOptions(original[i], result[i - offset]); 141 request = null;
83 }
84 return result;
85 };
86 142
87 export default class PlaylistLoader extends Stream { 143 if (startingState) {
88 constructor(srcUrl, hls, withCredentials) { 144 loader.state = startingState;
89 super(); 145 }
90 let loader = this;
91 let dispose;
92 let mediaUpdateTimeout;
93 let request;
94 let playlistRequestError;
95 let haveMetadata;
96 146
97 this.hls_ = hls; 147 loader.error = {
148 playlist: loader.master.playlists[url],
149 status: xhr.status,
150 message: 'HLS playlist request error at URL: ' + url,
151 responseText: xhr.responseText,
152 code: (xhr.status >= 500) ? 4 : 2
153 };
98 154
99 // a flag that disables "expired time"-tracking this setting has 155 loader.trigger('error');
100 // no effect when not playing a live stream 156 };
101 this.trackExpiredTime_ = false;
102 157
103 if (!srcUrl) { 158 // update the playlist loader's state in response to a new or
104 throw new Error('A non-empty playlist URL is required'); 159 // updated playlist.
105 } 160 haveMetadata = function(xhr, url) {
161 let parser;
162 let refreshDelay;
163 let update;
106 164
107 playlistRequestError = function(xhr, url, startingState) { 165 loader.setBandwidth(request || xhr);
108 loader.setBandwidth(request || xhr);
109 166
110 // any in-flight request is now finished 167 // any in-flight request is now finished
111 request = null; 168 request = null;
112 169
113 if (startingState) { 170 loader.state = 'HAVE_METADATA';
114 loader.state = startingState;
115 }
116 171
117 loader.error = { 172 parser = new m3u8.Parser();
118 playlist: loader.master.playlists[url], 173 parser.push(xhr.responseText);
119 status: xhr.status, 174 parser.end();
120 message: 'HLS playlist request error at URL: ' + url, 175 parser.manifest.uri = url;
121 responseText: xhr.responseText,
122 code: (xhr.status >= 500) ? 4 : 2
123 };
124 loader.trigger('error');
125 };
126 176
127 // update the playlist loader's state in response to a new or 177 // merge this playlist into the master
128 // updated playlist. 178 update = updateMaster(loader.master, parser.manifest);
179 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
180 if (update) {
181 loader.master = update;
182 loader.updateMediaPlaylist_(parser.manifest);
183 } else {
184 // if the playlist is unchanged since the last reload,
185 // try again after half the target duration
186 refreshDelay /= 2;
187 }
129 188
130 haveMetadata = function(xhr, url) { 189 // refresh live playlists after a target duration passes
131 let parser; 190 if (!loader.media().endList) {
132 let refreshDelay; 191 window.clearTimeout(mediaUpdateTimeout);
133 let update; 192 mediaUpdateTimeout = window.setTimeout(function() {
193 loader.trigger('mediaupdatetimeout');
194 }, refreshDelay);
195 }
134 196
135 loader.setBandwidth(request || xhr); 197 loader.trigger('loadedplaylist');
198 };
136 199
137 // any in-flight request is now finished 200 // initialize the loader state
138 request = null; 201 loader.state = 'HAVE_NOTHING';
139 loader.state = 'HAVE_METADATA';
140 202
141 parser = new m3u8.Parser(); 203 // track the time that has expired from the live window
142 parser.push(xhr.responseText); 204 // this allows the seekable start range to be calculated even if
143 parser.end(); 205 // all segments with timing information have expired
144 parser.manifest.uri = url; 206 this.expired_ = 0;
145
146 // merge this playlist into the master
147 update = updateMaster(loader.master, parser.manifest);
148 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
149 if (update) {
150 loader.master = update;
151 loader.updateMediaPlaylist_(parser.manifest);
152 } else {
153 // if the playlist is unchanged since the last reload,
154 // try again after half the target duration
155 refreshDelay /= 2;
156 }
157 207
158 // refresh live playlists after a target duration passes 208 // capture the prototype dispose function
159 if (!loader.media().endList) { 209 dispose = this.dispose;
160 window.clearTimeout(mediaUpdateTimeout);
161 mediaUpdateTimeout = window.setTimeout(function() {
162 loader.trigger('mediaupdatetimeout');
163 }, refreshDelay);
164 }
165 210
166 loader.trigger('loadedplaylist'); 211 /**
167 }; 212 * Abort any outstanding work and clean up.
213 */
214 loader.dispose = function() {
215 loader.stopRequest();
216 window.clearTimeout(mediaUpdateTimeout);
217 dispose.call(this);
218 };
168 219
169 // initialize the loader state 220 loader.stopRequest = () => {
170 loader.state = 'HAVE_NOTHING'; 221 if (request) {
222 let oldRequest = request;
171 223
172 // track the time that has expired from the live window 224 request = null;
173 // this allows the seekable start range to be calculated even if 225 oldRequest.onreadystatechange = null;
174 // all segments with timing information have expired 226 oldRequest.abort();
175 this.expired_ = 0; 227 }
228 };
229
230 /**
231 * When called without any arguments, returns the currently
232 * active media playlist. When called with a single argument,
233 * triggers the playlist loader to asynchronously switch to the
234 * specified media playlist. Calling this method while the
235 * loader is in the HAVE_NOTHING causes an error to be emitted
236 * but otherwise has no effect.
237 *
238 * @param {Object=} playlis tthe parsed media playlist
239 * object to switch to
240 * @return {Playlist} the current loaded media
241 */
242 loader.media = function(playlist) {
243 let startingState = loader.state;
244 let mediaChange;
245
246 // getter
247 if (!playlist) {
248 return loader.media_;
249 }
176 250
177 // capture the prototype dispose function 251 // setter
178 dispose = this.dispose; 252 if (loader.state === 'HAVE_NOTHING') {
253 throw new Error('Cannot switch media playlist from ' + loader.state);
254 }
179 255
180 /** 256 // find the playlist object if the target playlist has been
181 * Abort any outstanding work and clean up. 257 // specified by URI
182 */ 258 if (typeof playlist === 'string') {
183 loader.dispose = function() { 259 if (!loader.master.playlists[playlist]) {
260 throw new Error('Unknown playlist URI: ' + playlist);
261 }
262 playlist = loader.master.playlists[playlist];
263 }
264
265 mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri;
266
267 // switch to fully loaded playlists immediately
268 if (loader.master.playlists[playlist.uri].endList) {
269 // abort outstanding playlist requests
184 if (request) { 270 if (request) {
185 request.onreadystatechange = null; 271 request.onreadystatechange = null;
186 request.abort(); 272 request.abort();
187 request = null; 273 request = null;
188 } 274 }
189 window.clearTimeout(mediaUpdateTimeout); 275 loader.state = 'HAVE_METADATA';
190 dispose.call(this); 276 loader.media_ = playlist;
191 };
192
193 /**
194 * When called without any arguments, returns the currently
195 * active media playlist. When called with a single argument,
196 * triggers the playlist loader to asynchronously switch to the
197 * specified media playlist. Calling this method while the
198 * loader is in the HAVE_NOTHING causes an error to be emitted
199 * but otherwise has no effect.
200 * @param playlist (optional) {object} the parsed media playlist
201 * object to switch to
202 */
203 loader.media = function(playlist) {
204 let startingState = loader.state;
205 let mediaChange;
206 // getter
207 if (!playlist) {
208 return loader.media_;
209 }
210 277
211 // setter 278 // trigger media change if the active media has been updated
212 if (loader.state === 'HAVE_NOTHING') { 279 if (mediaChange) {
213 throw new Error('Cannot switch media playlist from ' + loader.state); 280 loader.trigger('mediachanging');
214 } 281 loader.trigger('mediachange');
215
216 // find the playlist object if the target playlist has been
217 // specified by URI
218 if (typeof playlist === 'string') {
219 if (!loader.master.playlists[playlist]) {
220 throw new Error('Unknown playlist URI: ' + playlist);
221 }
222 playlist = loader.master.playlists[playlist];
223 } 282 }
283 return;
284 }
224 285
225 mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri; 286 // switching to the active playlist is a no-op
287 if (!mediaChange) {
288 return;
289 }
226 290
227 // switch to fully loaded playlists immediately 291 loader.state = 'SWITCHING_MEDIA';
228 if (loader.master.playlists[playlist.uri].endList) {
229 // abort outstanding playlist requests
230 if (request) {
231 request.onreadystatechange = null;
232 request.abort();
233 request = null;
234 }
235 loader.state = 'HAVE_METADATA';
236 loader.media_ = playlist;
237 292
238 // trigger media change if the active media has been updated 293 // there is already an outstanding playlist request
239 if (mediaChange) { 294 if (request) {
240 loader.trigger('mediachange'); 295 if (resolveUrl(loader.master.uri, playlist.uri) === request.url) {
241 } 296 // requesting to switch to the same playlist multiple times
297 // has no effect after the first
242 return; 298 return;
243 } 299 }
300 request.onreadystatechange = null;
301 request.abort();
302 request = null;
303 }
244 304
245 // switching to the active playlist is a no-op 305 // request the new playlist
246 if (!mediaChange) { 306 if (this.media_) {
307 this.trigger('mediachanging');
308 }
309 request = this.hls_.xhr({
310 uri: resolveUrl(loader.master.uri, playlist.uri),
311 withCredentials
312 }, function(error, req) {
313 // disposed
314 if (!request) {
247 return; 315 return;
248 } 316 }
249 317
250 loader.state = 'SWITCHING_MEDIA'; 318 if (error) {
251 319 return playlistRequestError(request, playlist.uri, startingState);
252 // there is already an outstanding playlist request
253 if (request) {
254 if (resolveUrl(loader.master.uri, playlist.uri) === request.url) {
255 // requesting to switch to the same playlist multiple times
256 // has no effect after the first
257 return;
258 }
259 request.onreadystatechange = null;
260 request.abort();
261 request = null;
262 } 320 }
263 321
264 // request the new playlist 322 haveMetadata(req, playlist.uri);
265 request = this.hls_.xhr({
266 uri: resolveUrl(loader.master.uri, playlist.uri),
267 withCredentials
268 }, function(error, request) {
269 if (error) {
270 return playlistRequestError(request, playlist.uri, startingState);
271 }
272 323
273 haveMetadata(request, playlist.uri); 324 // fire loadedmetadata the first time a media playlist is loaded
325 if (startingState === 'HAVE_MASTER') {
326 loader.trigger('loadedmetadata');
327 } else {
328 loader.trigger('mediachange');
329 }
330 });
331 };
274 332
275 // fire loadedmetadata the first time a media playlist is loaded 333 /**
276 if (startingState === 'HAVE_MASTER') { 334 * set the bandwidth on an xhr to the bandwidth on the playlist
277 loader.trigger('loadedmetadata'); 335 */
278 } else { 336 loader.setBandwidth = function(xhr) {
279 loader.trigger('mediachange'); 337 loader.bandwidth = xhr.bandwidth;
280 } 338 };
281 }); 339
282 }; 340 // In a live playlist, don't keep track of the expired time
341 // until HLS tells us that "first play" has commenced
342 loader.on('firstplay', function() {
343 this.trackExpiredTime_ = true;
344 });
345
346 // live playlist staleness timeout
347 loader.on('mediaupdatetimeout', function() {
348 if (loader.state !== 'HAVE_METADATA') {
349 // only refresh the media playlist if no other activity is going on
350 return;
351 }
283 352
284 loader.setBandwidth = function(xhr) { 353 loader.state = 'HAVE_CURRENT_METADATA';
285 loader.bandwidth = xhr.bandwidth; 354 request = this.hls_.xhr({
286 }; 355 uri: resolveUrl(loader.master.uri, loader.media().uri),
356 withCredentials
357 }, function(error, req) {
358 // disposed
359 if (!request) {
360 return;
361 }
287 362
288 // In a live list, don't keep track of the expired time until 363 if (error) {
289 // HLS tells us that "first play" has commenced 364 return playlistRequestError(request, loader.media().uri);
290 loader.on('firstplay', function() { 365 }
291 this.trackExpiredTime_ = true; 366 haveMetadata(request, loader.media().uri);
292 }); 367 });
368 });
293 369
294 // live playlist staleness timeout 370 /**
295 loader.on('mediaupdatetimeout', function() { 371 * pause loading of the playlist
296 if (loader.state !== 'HAVE_METADATA') { 372 */
297 // only refresh the media playlist if no other activity is going on 373 loader.pause = () => {
298 return; 374 loader.stopRequest();
375 window.clearTimeout(mediaUpdateTimeout);
376 };
377
378 /**
379 * start loading of the playlist
380 */
381 loader.load = () => {
382 if (loader.started) {
383 if (!loader.media().endList) {
384 loader.trigger('mediaupdatetimeout');
385 } else {
386 loader.trigger('loadedplaylist');
299 } 387 }
388 } else {
389 loader.start();
390 }
391 };
300 392
301 loader.state = 'HAVE_CURRENT_METADATA'; 393 /**
302 request = this.hls_.xhr({ 394 * start loading of the playlist
303 uri: resolveUrl(loader.master.uri, loader.media().uri), 395 */
304 withCredentials 396 loader.start = () => {
305 }, function(error, request) { 397 loader.started = true;
306 if (error) {
307 return playlistRequestError(request, loader.media().uri);
308 }
309 haveMetadata(request, loader.media().uri);
310 });
311 });
312 398
313 // request the specified URL 399 // request the specified URL
314 request = this.hls_.xhr({ 400 request = this.hls_.xhr({
...@@ -316,8 +402,14 @@ export default class PlaylistLoader extends Stream { ...@@ -316,8 +402,14 @@ export default class PlaylistLoader extends Stream {
316 withCredentials 402 withCredentials
317 }, function(error, req) { 403 }, function(error, req) {
318 let parser; 404 let parser;
405 let playlist;
319 let i; 406 let i;
320 407
408 // disposed
409 if (!request) {
410 return;
411 }
412
321 // clear the loader's request reference 413 // clear the loader's request reference
322 request = null; 414 request = null;
323 415
...@@ -344,10 +436,23 @@ export default class PlaylistLoader extends Stream { ...@@ -344,10 +436,23 @@ export default class PlaylistLoader extends Stream {
344 if (parser.manifest.playlists) { 436 if (parser.manifest.playlists) {
345 loader.master = parser.manifest; 437 loader.master = parser.manifest;
346 438
347 // setup by-URI lookups 439 // setup by-URI lookups and resolve media playlist URIs
348 i = loader.master.playlists.length; 440 i = loader.master.playlists.length;
349 while (i--) { 441 while (i--) {
350 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; 442 playlist = loader.master.playlists[i];
443 loader.master.playlists[playlist.uri] = playlist;
444 playlist.resolvedUri = resolveUrl(loader.master.uri, playlist.uri);
445 }
446
447 // resolve any media group URIs
448 for (let groupKey in loader.master.mediaGroups.AUDIO) {
449 for (let labelKey in loader.master.mediaGroups.AUDIO[groupKey]) {
450 let alternateAudio = loader.master.mediaGroups.AUDIO[groupKey][labelKey];
451 if (alternateAudio.uri) {
452 alternateAudio.resolvedUri =
453 resolveUrl(loader.master.uri, alternateAudio.uri);
454 }
455 }
351 } 456 }
352 457
353 loader.trigger('loadedplaylist'); 458 loader.trigger('loadedplaylist');
...@@ -368,200 +473,82 @@ export default class PlaylistLoader extends Stream { ...@@ -368,200 +473,82 @@ export default class PlaylistLoader extends Stream {
368 }] 473 }]
369 }; 474 };
370 loader.master.playlists[srcUrl] = loader.master.playlists[0]; 475 loader.master.playlists[srcUrl] = loader.master.playlists[0];
476 loader.master.playlists[0].resolvedUri = srcUrl;
371 haveMetadata(req, srcUrl); 477 haveMetadata(req, srcUrl);
372 return loader.trigger('loadedmetadata'); 478 return loader.trigger('loadedmetadata');
373 }); 479 });
374 } 480 };
375 /** 481 };
376 * Update the PlaylistLoader state to reflect the changes in an
377 * update to the current media playlist.
378 * @param update {object} the updated media playlist object
379 */
380 updateMediaPlaylist_(update) {
381 let outdated;
382 let i;
383 let segment;
384
385 outdated = this.media_;
386 this.media_ = this.master.playlists[update.uri];
387
388 if (!outdated) {
389 return;
390 }
391 482
392 // don't track expired time until this flag is truthy 483 PlaylistLoader.prototype = new Stream();
393 if (!this.trackExpiredTime_) {
394 return;
395 }
396 484
397 // if the update was the result of a rendition switch do not 485 /**
398 // attempt to calculate expired_ since media-sequences need not 486 * Update the PlaylistLoader state to reflect the changes in an
399 // correlate between renditions/variants 487 * update to the current media playlist.
400 if (update.uri !== outdated.uri) { 488 *
401 return; 489 * @param {Object} update the updated media playlist object
402 } 490 */
491 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
492 let outdated;
493 let i;
494 let segment;
403 495
404 // try using precise timing from first segment of the updated 496 outdated = this.media_;
405 // playlist 497 this.media_ = this.master.playlists[update.uri];
406 if (update.segments.length) {
407 if (update.segments[0].start !== undefined) {
408 this.expired_ = update.segments[0].start;
409 return;
410 } else if (update.segments[0].end !== undefined) {
411 this.expired_ = update.segments[0].end - update.segments[0].duration;
412 return;
413 }
414 }
415 498
416 // calculate expired by walking the outdated playlist 499 if (!outdated) {
417 i = update.mediaSequence - outdated.mediaSequence - 1; 500 return;
501 }
418 502
419 for (; i >= 0; i--) { 503 // don't track expired time until this flag is truthy
420 segment = outdated.segments[i]; 504 if (!this.trackExpiredTime_) {
505 return;
506 }
421 507
422 if (!segment) { 508 // if the update was the result of a rendition switch do not
423 // we missed information on this segment completely between 509 // attempt to calculate expired_ since media-sequences need not
424 // playlist updates so we'll have to take an educated guess 510 // correlate between renditions/variants
425 // once we begin buffering again, any error we introduce can 511 if (update.uri !== outdated.uri) {
426 // be corrected 512 return;
427 this.expired_ += outdated.targetDuration || 10; 513 }
428 continue;
429 }
430 514
431 if (segment.end !== undefined) { 515 // try using precise timing from first segment of the updated
432 this.expired_ = segment.end; 516 // playlist
433 return; 517 if (update.segments.length) {
434 } 518 if (typeof update.segments[0].start !== 'undefined') {
435 if (segment.start !== undefined) { 519 this.expired_ = update.segments[0].start;
436 this.expired_ = segment.start + segment.duration; 520 return;
437 return; 521 } else if (typeof update.segments[0].end !== 'undefined') {
438 } 522 this.expired_ = update.segments[0].end - update.segments[0].duration;
439 this.expired_ += segment.duration; 523 return;
440 } 524 }
441 } 525 }
442 526
443 /** 527 // calculate expired by walking the outdated playlist
444 * Determine the index of the segment that contains a specified 528 i = update.mediaSequence - outdated.mediaSequence - 1;
445 * playback position in the current media playlist. Early versions
446 * of the HLS specification require segment durations to be rounded
447 * to the nearest integer which means it may not be possible to
448 * determine the correct segment for a playback position if that
449 * position is within .5 seconds of the segment duration. This
450 * function will always return the lower of the two possible indices
451 * in those cases.
452 *
453 * @param time {number} The number of seconds since the earliest
454 * possible position to determine the containing segment for
455 * @returns {number} The number of the media segment that contains
456 * that time position. If the specified playback position is outside
457 * the time range of the current set of media segments, the return
458 * value will be clamped to the index of the segment containing the
459 * closest playback position that is currently available.
460 */
461 getMediaIndexForTime_(time) {
462 let i;
463 let segment;
464 let originalTime = time;
465 let numSegments = this.media_.segments.length;
466 let lastSegment = numSegments - 1;
467 let startIndex;
468 let endIndex;
469 let knownStart;
470 let knownEnd;
471
472 if (!this.media_) {
473 return 0;
474 }
475 529
476 // when the requested position is earlier than the current set of 530 for (; i >= 0; i--) {
477 // segments, return the earliest segment index 531 segment = outdated.segments[i];
478 if (time < 0) {
479 return 0;
480 }
481 532
482 // find segments with known timing information that bound the 533 if (!segment) {
483 // target time 534 // we missed information on this segment completely between
484 for (i = 0; i < numSegments; i++) { 535 // playlist updates so we'll have to take an educated guess
485 segment = this.media_.segments[i]; 536 // once we begin buffering again, any error we introduce can
486 if (segment.end) { 537 // be corrected
487 if (segment.end > time) { 538 this.expired_ += outdated.targetDuration || 10;
488 knownEnd = segment.end; 539 continue;
489 endIndex = i;
490 break;
491 } else {
492 knownStart = segment.end;
493 startIndex = i + 1;
494 }
495 }
496 } 540 }
497 541
498 // use the bounds we just found and playlist information to 542 if (typeof segment.end !== 'undefined') {
499 // estimate the segment that contains the time we are looking for 543 this.expired_ = segment.end;
500 if (startIndex !== undefined) { 544 return;
501 // We have a known-start point that is before our desired time so 545 }
502 // walk from that point forwards 546 if (typeof segment.start !== 'undefined') {
503 time = time - knownStart; 547 this.expired_ = segment.start + segment.duration;
504 for (i = startIndex; i < (endIndex || numSegments); i++) { 548 return;
505 segment = this.media_.segments[i];
506 time -= segment.duration;
507
508 if (time < 0) {
509 return i;
510 }
511 }
512
513 if (i >= endIndex) {
514 // We haven't found a segment but we did hit a known end point
515 // so fallback to interpolating between the segment index
516 // based on the known span of the timeline we are dealing with
517 // and the number of segments inside that span
518 return startIndex + Math.floor(
519 ((originalTime - knownStart) / (knownEnd - knownStart)) *
520 (endIndex - startIndex));
521 }
522
523 // We _still_ haven't found a segment so load the last one
524 return lastSegment;
525 } else if (endIndex !== undefined) {
526 // We _only_ have a known-end point that is after our desired time so
527 // walk from that point backwards
528 time = knownEnd - time;
529 for (i = endIndex; i >= 0; i--) {
530 segment = this.media_.segments[i];
531 time -= segment.duration;
532
533 if (time < 0) {
534 return i;
535 }
536 }
537
538 // We haven't found a segment so load the first one if time is zero
539 if (time === 0) {
540 return 0;
541 } else {
542 return -1;
543 }
544 } else {
545 // We known nothing so walk from the front of the playlist,
546 // subtracting durations until we find a segment that contains
547 // time and return it
548 time = time - this.expired_;
549
550 if (time < 0) {
551 return -1;
552 }
553
554 for (i = 0; i < numSegments; i++) {
555 segment = this.media_.segments[i];
556 time -= segment.duration;
557 if (time < 0) {
558 return i;
559 }
560 }
561 // We are out of possible candidates so load the last one...
562 // The last one is the least likely to overlap a buffer and therefore
563 // the one most likely to tell us something about the timeline
564 return lastSegment;
565 } 549 }
550 this.expired_ += segment.duration;
566 } 551 }
567 } 552 };
553
554 export default PlaylistLoader;
......
1 /** 1 /**
2 * @file playlist.js
3 *
2 * Playlist related utilities. 4 * Playlist related utilities.
3 */ 5 */
4 import {createTimeRange} from 'video.js'; 6 import {createTimeRange} from 'video.js';
...@@ -13,6 +15,14 @@ let Playlist = { ...@@ -13,6 +15,14 @@ let Playlist = {
13 UNSAFE_LIVE_SEGMENTS: 3 15 UNSAFE_LIVE_SEGMENTS: 3
14 }; 16 };
15 17
18 /**
19 * walk backward until we find a duration we can use
20 * or return a failure
21 *
22 * @param {Playlist} playlist the playlist to walk through
23 * @param {Number} endSequence the mediaSequence to stop walking on
24 */
25
16 const backwardDuration = function(playlist, endSequence) { 26 const backwardDuration = function(playlist, endSequence) {
17 let result = 0; 27 let result = 0;
18 let i = endSequence - playlist.mediaSequence; 28 let i = endSequence - playlist.mediaSequence;
...@@ -48,6 +58,13 @@ const backwardDuration = function(playlist, endSequence) { ...@@ -48,6 +58,13 @@ const backwardDuration = function(playlist, endSequence) {
48 return { result, precise: false }; 58 return { result, precise: false };
49 }; 59 };
50 60
61 /**
62 * walk forward until we find a duration we can use
63 * or return a failure
64 *
65 * @param {Playlist} playlist the playlist to walk through
66 * @param {Number} endSequence the mediaSequence to stop walking on
67 */
51 const forwardDuration = function(playlist, endSequence) { 68 const forwardDuration = function(playlist, endSequence) {
52 let result = 0; 69 let result = 0;
53 let segment; 70 let segment;
...@@ -83,13 +100,15 @@ const forwardDuration = function(playlist, endSequence) { ...@@ -83,13 +100,15 @@ const forwardDuration = function(playlist, endSequence) {
83 * playlist. The duration of a subinterval of the available segments 100 * playlist. The duration of a subinterval of the available segments
84 * may be calculated by specifying an end index. 101 * may be calculated by specifying an end index.
85 * 102 *
86 * @param playlist {object} a media playlist object 103 * @param {Object} playlist a media playlist object
87 * @param endSequence {number} (optional) an exclusive upper boundary 104 * @param {Number=} endSequence an exclusive upper boundary
88 * for the playlist. Defaults to playlist length. 105 * for the playlist. Defaults to playlist length.
89 * @return {number} the duration between the first available segment 106 * @param {Number} expired the amount of time that has dropped
107 * off the front of the playlist in a live scenario
108 * @return {Number} the duration between the first available segment
90 * and end index. 109 * and end index.
91 */ 110 */
92 const intervalDuration = function(playlist, endSequence) { 111 const intervalDuration = function(playlist, endSequence, expired) {
93 let backward; 112 let backward;
94 let forward; 113 let forward;
95 114
...@@ -120,7 +139,7 @@ const intervalDuration = function(playlist, endSequence) { ...@@ -120,7 +139,7 @@ const intervalDuration = function(playlist, endSequence) {
120 } 139 }
121 140
122 // return the less-precise, playlist-based duration estimate 141 // return the less-precise, playlist-based duration estimate
123 return backward.result; 142 return backward.result + expired;
124 }; 143 };
125 144
126 /** 145 /**
...@@ -128,23 +147,23 @@ const intervalDuration = function(playlist, endSequence) { ...@@ -128,23 +147,23 @@ const intervalDuration = function(playlist, endSequence) {
128 * are specified, the duration will be for the subset of the media 147 * are specified, the duration will be for the subset of the media
129 * timeline between those two indices. The total duration for live 148 * timeline between those two indices. The total duration for live
130 * playlists is always Infinity. 149 * playlists is always Infinity.
131 * @param playlist {object} a media playlist object 150 *
132 * @param endSequence {number} (optional) an exclusive upper 151 * @param {Object} playlist a media playlist object
133 * boundary for the playlist. Defaults to the playlist media 152 * @param {Number=} endSequence an exclusive upper
153 * boundary for the playlist. Defaults to the playlist media
134 * sequence number plus its length. 154 * sequence number plus its length.
135 * @param includeTrailingTime {boolean} (optional) if false, the 155 * @param {Number=} expired the amount of time that has
136 * interval between the final segment and the subsequent segment 156 * dropped off the front of the playlist in a live scenario
137 * will not be included in the result 157 * @return {Number} the duration between the start index and end
138 * @return {number} the duration between the start index and end
139 * index. 158 * index.
140 */ 159 */
141 export const duration = function(playlist, endSequence, includeTrailingTime) { 160 export const duration = function(playlist, endSequence, expired) {
142 if (!playlist) { 161 if (!playlist) {
143 return 0; 162 return 0;
144 } 163 }
145 164
146 if (typeof includeTrailingTime === 'undefined') { 165 if (typeof expired !== 'number') {
147 includeTrailingTime = true; 166 expired = 0;
148 } 167 }
149 168
150 // if a slice of the total duration is not requested, use 169 // if a slice of the total duration is not requested, use
...@@ -164,7 +183,7 @@ export const duration = function(playlist, endSequence, includeTrailingTime) { ...@@ -164,7 +183,7 @@ export const duration = function(playlist, endSequence, includeTrailingTime) {
164 // calculate the total duration based on the segment durations 183 // calculate the total duration based on the segment durations
165 return intervalDuration(playlist, 184 return intervalDuration(playlist,
166 endSequence, 185 endSequence,
167 includeTrailingTime); 186 expired);
168 }; 187 };
169 188
170 /** 189 /**
...@@ -174,16 +193,24 @@ export const duration = function(playlist, endSequence, includeTrailingTime) { ...@@ -174,16 +193,24 @@ export const duration = function(playlist, endSequence, includeTrailingTime) {
174 * seekable implementation for live streams would need to offset 193 * seekable implementation for live streams would need to offset
175 * these values by the duration of content that has expired from the 194 * these values by the duration of content that has expired from the
176 * stream. 195 * stream.
177 * @param playlist {object} a media playlist object 196 *
197 * @param {Object} playlist a media playlist object
198 * @param {Number=} expired the amount of time that has
199 * dropped off the front of the playlist in a live scenario
178 * @return {TimeRanges} the periods of time that are valid targets 200 * @return {TimeRanges} the periods of time that are valid targets
179 * for seeking 201 * for seeking
180 */ 202 */
181 export const seekable = function(playlist) { 203 export const seekable = function(playlist, expired) {
182 let start; 204 let start;
183 let end; 205 let end;
206 let endSequence;
207
208 if (typeof expired !== 'number') {
209 expired = 0;
210 }
184 211
185 // without segments, there are no seekable ranges 212 // without segments, there are no seekable ranges
186 if (!playlist.segments) { 213 if (!playlist || !playlist.segments) {
187 return createTimeRange(); 214 return createTimeRange();
188 } 215 }
189 // when the playlist is complete, the entire duration is seekable 216 // when the playlist is complete, the entire duration is seekable
...@@ -194,15 +221,142 @@ export const seekable = function(playlist) { ...@@ -194,15 +221,142 @@ export const seekable = function(playlist) {
194 // live playlists should not expose three segment durations worth 221 // live playlists should not expose three segment durations worth
195 // of content from the end of the playlist 222 // of content from the end of the playlist
196 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 223 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
197 start = intervalDuration(playlist, playlist.mediaSequence); 224 start = intervalDuration(playlist, playlist.mediaSequence, expired);
225 endSequence = Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS);
198 end = intervalDuration(playlist, 226 end = intervalDuration(playlist,
199 playlist.mediaSequence + 227 playlist.mediaSequence + endSequence,
200 Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS)); 228 expired);
201 return createTimeRange(start, end); 229 return createTimeRange(start, end);
202 }; 230 };
203 231
232 /**
233 * Determine the index of the segment that contains a specified
234 * playback position in a media playlist.
235 *
236 * @param {Object} playlist the media playlist to query
237 * @param {Number} time The number of seconds since the earliest
238 * possible position to determine the containing segment for
239 * @param {Number=} expired the duration of content, in
240 * seconds, that has been removed from this playlist because it
241 * expired
242 * @return {Number} The number of the media segment that contains
243 * that time position.
244 */
245 export const getMediaIndexForTime_ = function(playlist, time, expired) {
246 let i;
247 let segment;
248 let originalTime = time;
249 let numSegments = playlist.segments.length;
250 let lastSegment = numSegments - 1;
251 let startIndex;
252 let endIndex;
253 let knownStart;
254 let knownEnd;
255
256 if (!playlist) {
257 return 0;
258 }
259
260 // when the requested position is earlier than the current set of
261 // segments, return the earliest segment index
262 if (time < 0) {
263 return 0;
264 }
265
266 expired = expired || 0;
267
268 // find segments with known timing information that bound the
269 // target time
270 for (i = 0; i < numSegments; i++) {
271 segment = playlist.segments[i];
272 if (segment.end) {
273 if (segment.end > time) {
274 knownEnd = segment.end;
275 endIndex = i;
276 break;
277 } else {
278 knownStart = segment.end;
279 startIndex = i + 1;
280 }
281 }
282 }
283
284 // time was equal to or past the end of the last segment in the playlist
285 if (startIndex === numSegments) {
286 return numSegments;
287 }
288
289 // use the bounds we just found and playlist information to
290 // estimate the segment that contains the time we are looking for
291 if (typeof startIndex !== 'undefined') {
292 // We have a known-start point that is before our desired time so
293 // walk from that point forwards
294 time = time - knownStart;
295 for (i = startIndex; i < (endIndex || numSegments); i++) {
296 segment = playlist.segments[i];
297 time -= segment.duration;
298
299 if (time < 0) {
300 return i;
301 }
302 }
303
304 if (i >= endIndex) {
305 // We haven't found a segment but we did hit a known end point
306 // so fallback to interpolating between the segment index
307 // based on the known span of the timeline we are dealing with
308 // and the number of segments inside that span
309 return startIndex + Math.floor(
310 ((originalTime - knownStart) / (knownEnd - knownStart)) *
311 (endIndex - startIndex));
312 }
313
314 // We _still_ haven't found a segment so load the last one
315 return lastSegment;
316 } else if (typeof endIndex !== 'undefined') {
317 // We _only_ have a known-end point that is after our desired time so
318 // walk from that point backwards
319 time = knownEnd - time;
320 for (i = endIndex; i >= 0; i--) {
321 segment = playlist.segments[i];
322 time -= segment.duration;
323
324 if (time < 0) {
325 return i;
326 }
327 }
328
329 // We haven't found a segment so load the first one if time is zero
330 if (time === 0) {
331 return 0;
332 }
333 return -1;
334 }
335 // We known nothing so walk from the front of the playlist,
336 // subtracting durations until we find a segment that contains
337 // time and return it
338 time = time - expired;
339
340 if (time < 0) {
341 return -1;
342 }
343
344 for (i = 0; i < numSegments; i++) {
345 segment = playlist.segments[i];
346 time -= segment.duration;
347 if (time < 0) {
348 return i;
349 }
350 }
351 // We are out of possible candidates so load the last one...
352 // The last one is the least likely to overlap a buffer and therefore
353 // the one most likely to tell us something about the timeline
354 return lastSegment;
355 };
356
204 Playlist.duration = duration; 357 Playlist.duration = duration;
205 Playlist.seekable = seekable; 358 Playlist.seekable = seekable;
359 Playlist.getMediaIndexForTime_ = getMediaIndexForTime_;
206 360
207 // exports 361 // exports
208 export default Playlist; 362 export default Playlist;
......
1 /**
2 * ranges
3 *
4 * Utilities for working with TimeRanges.
5 *
6 */
7
8 import videojs from 'video.js';
9
10 // Fudge factor to account for TimeRanges rounding
11 const TIME_FUDGE_FACTOR = 1 / 30;
12
13 const filterRanges = function(timeRanges, predicate) {
14 let results = [];
15 let i;
16
17 if (timeRanges && timeRanges.length) {
18 // Search for ranges that match the predicate
19 for (i = 0; i < timeRanges.length; i++) {
20 if (predicate(timeRanges.start(i), timeRanges.end(i))) {
21 results.push([timeRanges.start(i), timeRanges.end(i)]);
22 }
23 }
24 }
25
26 return videojs.createTimeRanges(results);
27 };
28
29 /**
30 * Attempts to find the buffered TimeRange that contains the specified
31 * time.
32 * @param {TimeRanges} buffered - the TimeRanges object to query
33 * @param {number} time - the time to filter on.
34 * @returns {TimeRanges} a new TimeRanges object
35 */
36 const findRange = function(buffered, time) {
37 return filterRanges(buffered, function(start, end) {
38 return start - TIME_FUDGE_FACTOR <= time &&
39 end + TIME_FUDGE_FACTOR >= time;
40 });
41 };
42
43 /**
44 * Returns the TimeRanges that begin at or later than the specified
45 * time.
46 * @param {TimeRanges} timeRanges - the TimeRanges object to query
47 * @param {number} time - the time to filter on.
48 * @returns {TimeRanges} a new TimeRanges object.
49 */
50 const findNextRange = function(timeRanges, time) {
51 return filterRanges(timeRanges, function(start) {
52 return start - TIME_FUDGE_FACTOR >= time;
53 });
54 };
55
56 /**
57 * Search for a likely end time for the segment that was just appened
58 * based on the state of the `buffered` property before and after the
59 * append. If we fin only one such uncommon end-point return it.
60 * @param {TimeRanges} original - the buffered time ranges before the update
61 * @param {TimeRanges} update - the buffered time ranges after the update
62 * @returns {Number|null} the end time added between `original` and `update`,
63 * or null if one cannot be unambiguously determined.
64 */
65 const findSoleUncommonTimeRangesEnd = function(original, update) {
66 let i;
67 let start;
68 let end;
69 let result = [];
70 let edges = [];
71
72 // In order to qualify as a possible candidate, the end point must:
73 // 1) Not have already existed in the `original` ranges
74 // 2) Not result from the shrinking of a range that already existed
75 // in the `original` ranges
76 // 3) Not be contained inside of a range that existed in `original`
77 const overlapsCurrentEnd = function(span) {
78 return (span[0] <= end && span[1] >= end);
79 };
80
81 if (original) {
82 // Save all the edges in the `original` TimeRanges object
83 for (i = 0; i < original.length; i++) {
84 start = original.start(i);
85 end = original.end(i);
86
87 edges.push([start, end]);
88 }
89 }
90
91 if (update) {
92 // Save any end-points in `update` that are not in the `original`
93 // TimeRanges object
94 for (i = 0; i < update.length; i++) {
95 start = update.start(i);
96 end = update.end(i);
97
98 if (edges.some(overlapsCurrentEnd)) {
99 continue;
100 }
101
102 // at this point it must be a unique non-shrinking end edge
103 result.push(end);
104 }
105 }
106
107 // we err on the side of caution and return null if didn't find
108 // exactly *one* differing end edge in the search above
109 if (result.length !== 1) {
110 return null;
111 }
112
113 return result[0];
114 };
115
116 /**
117 * Calculate the intersection of two TimeRanges
118 * @param {TimeRanges} bufferA
119 * @param {TimeRanges} bufferB
120 * @returns {TimeRanges} The interesection of `bufferA` with `bufferB`
121 */
122 const bufferIntersection = function(bufferA, bufferB) {
123 let start = null;
124 let end = null;
125 let arity = 0;
126 let extents = [];
127 let ranges = [];
128
129 if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
130 return videojs.createTimeRange();
131 }
132
133 // Handle the case where we have both buffers and create an
134 // intersection of the two
135 let count = bufferA.length;
136
137 // A) Gather up all start and end times
138 while (count--) {
139 extents.push({time: bufferA.start(count), type: 'start'});
140 extents.push({time: bufferA.end(count), type: 'end'});
141 }
142 count = bufferB.length;
143 while (count--) {
144 extents.push({time: bufferB.start(count), type: 'start'});
145 extents.push({time: bufferB.end(count), type: 'end'});
146 }
147 // B) Sort them by time
148 extents.sort(function(a, b) {
149 return a.time - b.time;
150 });
151
152 // C) Go along one by one incrementing arity for start and decrementing
153 // arity for ends
154 for (count = 0; count < extents.length; count++) {
155 if (extents[count].type === 'start') {
156 arity++;
157
158 // D) If arity is ever incremented to 2 we are entering an
159 // overlapping range
160 if (arity === 2) {
161 start = extents[count].time;
162 }
163 } else if (extents[count].type === 'end') {
164 arity--;
165
166 // E) If arity is ever decremented to 1 we leaving an
167 // overlapping range
168 if (arity === 1) {
169 end = extents[count].time;
170 }
171 }
172
173 // F) Record overlapping ranges
174 if (start !== null && end !== null) {
175 ranges.push([start, end]);
176 start = null;
177 end = null;
178 }
179 }
180
181 return videojs.createTimeRanges(ranges);
182 };
183
184 /**
185 * Calculates the percentage of `segmentRange` that overlaps the
186 * `buffered` time ranges.
187 * @param {TimeRanges} segmentRange - the time range that the segment covers
188 * @param {TimeRanges} buffered - the currently buffered time ranges
189 * @returns {Number} percent of the segment currently buffered
190 */
191 const calculateBufferedPercent = function(segmentRange, buffered) {
192 let segmentDuration = segmentRange.end(0) - segmentRange.start(0);
193 let intersection = bufferIntersection(segmentRange, buffered);
194 let overlapDuration = 0;
195 let count = intersection.length;
196
197 while (count--) {
198 overlapDuration += intersection.end(count) - intersection.start(count);
199 }
200
201 return (overlapDuration / segmentDuration) * 100;
202 };
203
204 export default {
205 findRange,
206 findNextRange,
207 findSoleUncommonTimeRangesEnd,
208 calculateBufferedPercent,
209 TIME_FUDGE_FACTOR
210 };
1 /**
2 * @file resolve-url.js
3 */
1 import document from 'global/document'; 4 import document from 'global/document';
2 /* eslint-disable max-len */
3 /** 5 /**
4 * Constructs a new URI by interpreting a path relative to another 6 * Constructs a new URI by interpreting a path relative to another
5 * URI. 7 * URI.
6 * @param basePath {string} a relative or absolute URI 8 *
7 * @param path {string} a path part to combine with the base
8 * @return {string} a URI that is equivalent to composing `base`
9 * with `path`
10 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 9 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
10 * @param {String} basePath a relative or absolute URI
11 * @param {String} path a path part to combine with the base
12 * @return {String} a URI that is equivalent to composing `base`
13 * with `path`
11 */ 14 */
12 /* eslint-enable max-len */
13 const resolveUrl = function(basePath, path) { 15 const resolveUrl = function(basePath, path) {
14 // use the base element to get the browser to handle URI resolution 16 // use the base element to get the browser to handle URI resolution
15 let oldBase = document.querySelector('base'); 17 let oldBase = document.querySelector('base');
......
1 /**
2 * @file segment-loader.js
3 */
4 import Ranges from './ranges';
5 import {getMediaIndexForTime_ as getMediaIndexForTime, duration} from './playlist';
6 import videojs from 'video.js';
7 import SourceUpdater from './source-updater';
8 import {Decrypter} from './decrypter';
9
10 // in ms
11 const CHECK_BUFFER_DELAY = 500;
12
13 // the desired length of video to maintain in the buffer, in seconds
14 export const GOAL_BUFFER_LENGTH = 30;
15
16 /**
17 * Updates segment with information about its end-point in time and, optionally,
18 * the segment duration if we have enough information to determine a segment duration
19 * accurately.
20 *
21 * @param {Object} playlist a media playlist object
22 * @param {Number} segmentIndex the index of segment we last appended
23 * @param {Number} segmentEnd the known of the segment referenced by segmentIndex
24 */
25 const updateSegmentMetadata = function(playlist, segmentIndex, segmentEnd) {
26 if (!playlist) {
27 return false;
28 }
29
30 let segment = playlist.segments[segmentIndex];
31 let previousSegment = playlist.segments[segmentIndex - 1];
32
33 if (segmentEnd && segment) {
34 segment.end = segmentEnd;
35
36 // fix up segment durations based on segment end data
37 if (!previousSegment) {
38 // first segment is always has a start time of 0 making its duration
39 // equal to the segment end
40 segment.duration = segment.end;
41 } else if (previousSegment.end) {
42 segment.duration = segment.end - previousSegment.end;
43 }
44 return true;
45 }
46 return false;
47 };
48
49 /**
50 * Determines if we should call endOfStream on the media source based
51 * on the state of the buffer or if appened segment was the final
52 * segment in the playlist.
53 *
54 * @param {Object} playlist a media playlist object
55 * @param {Object} mediaSource the MediaSource object
56 * @param {Number} segmentIndex the index of segment we last appended
57 * @param {Object} currentBuffered buffered region that currentTime resides in
58 * @returns {Boolean} do we need to call endOfStream on the MediaSource
59 */
60 const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentBuffered) {
61 if (!playlist) {
62 return false;
63 }
64
65 let segments = playlist.segments;
66
67 // determine a few boolean values to help make the branch below easier
68 // to read
69 let appendedLastSegment = (segmentIndex === segments.length - 1);
70 let bufferedToEnd = (currentBuffered.length &&
71 segments[segments.length - 1].end <= currentBuffered.end(0));
72
73 // if we've buffered to the end of the video, we need to call endOfStream
74 // so that MediaSources can trigger the `ended` event when it runs out of
75 // buffered data instead of waiting for me
76 return playlist.endList &&
77 mediaSource.readyState === 'open' &&
78 (appendedLastSegment || bufferedToEnd);
79 };
80
81 /* Turns segment byterange into a string suitable for use in
82 * HTTP Range requests
83 */
84 const byterangeStr = function(byterange) {
85 let byterangeStart;
86 let byterangeEnd;
87
88 // `byterangeEnd` is one less than `offset + length` because the HTTP range
89 // header uses inclusive ranges
90 byterangeEnd = byterange.offset + byterange.length - 1;
91 byterangeStart = byterange.offset;
92 return 'bytes=' + byterangeStart + '-' + byterangeEnd;
93 };
94
95 /* Defines headers for use in the xhr request for a particular segment.
96 */
97 const segmentXhrHeaders = function(segment) {
98 let headers = {};
99
100 if ('byterange' in segment) {
101 headers.Range = byterangeStr(segment.byterange);
102 }
103 return headers;
104 };
105
106 /**
107 * An object that manages segment loading and appending.
108 *
109 * @class SegmentLoader
110 * @param {Object} options required and optional options
111 * @extends videojs.EventTarget
112 */
113 export default class SegmentLoader extends videojs.EventTarget {
114 constructor(options) {
115 super();
116 let settings;
117
118 // check pre-conditions
119 if (!options) {
120 throw new TypeError('Initialization options are required');
121 }
122 if (typeof options.currentTime !== 'function') {
123 throw new TypeError('No currentTime getter specified');
124 }
125 if (!options.mediaSource) {
126 throw new TypeError('No MediaSource specified');
127 }
128 settings = videojs.mergeOptions(videojs.options.hls, options);
129
130 // public properties
131 this.state = 'INIT';
132 this.bandwidth = settings.bandwidth;
133 this.roundTrip = NaN;
134 this.bytesReceived = 0;
135
136 // private properties
137 this.hasPlayed_ = settings.hasPlayed;
138 this.currentTime_ = settings.currentTime;
139 this.seekable_ = settings.seekable;
140 this.seeking_ = settings.seeking;
141 this.setCurrentTime_ = settings.setCurrentTime;
142 this.mediaSource_ = settings.mediaSource;
143 this.withCredentials_ = settings.withCredentials;
144 this.checkBufferTimeout_ = null;
145 this.error_ = void 0;
146 this.expired_ = 0;
147 this.timeCorrection_ = 0;
148 this.currentTimeline_ = -1;
149 this.xhr_ = null;
150 this.pendingSegment_ = null;
151 this.sourceUpdater_ = null;
152 this.hls_ = settings.hls;
153 }
154
155 /**
156 * dispose of the SegmentLoader and reset to the default state
157 */
158 dispose() {
159 this.state = 'DISPOSED';
160 this.abort_();
161 if (this.sourceUpdater_) {
162 this.sourceUpdater_.dispose();
163 }
164 }
165
166 /**
167 * abort anything that is currently doing on with the SegmentLoader
168 * and reset to a default state
169 */
170 abort() {
171 if (this.state !== 'WAITING') {
172 return;
173 }
174
175 this.abort_();
176
177 // don't wait for buffer check timeouts to begin fetching the
178 // next segment
179 if (!this.paused()) {
180 this.state = 'READY';
181 this.fillBuffer_();
182 }
183 }
184
185 /**
186 * set an error on the segment loader and null out any pending segements
187 *
188 * @param {Error} error the error to set on the SegmentLoader
189 * @return {Error} the error that was set or that is currently set
190 */
191 error(error) {
192 if (typeof error !== 'undefined') {
193 this.error_ = error;
194 }
195
196 this.pendingSegment_ = null;
197 return this.error_;
198 }
199
200 /**
201 * load a playlist and start to fill the buffer
202 */
203 load() {
204 this.monitorBuffer_();
205
206 // if we don't have a playlist yet, keep waiting for one to be
207 // specified
208 if (!this.playlist_) {
209 return;
210 }
211
212 // if we're in the middle of processing a segment already, don't
213 // kick off an additional segment request
214 if (!this.sourceUpdater_ ||
215 (this.state !== 'READY' &&
216 this.state !== 'INIT')) {
217 return;
218 }
219
220 this.state = 'READY';
221 this.fillBuffer_();
222 }
223
224 /**
225 * set a playlist on the segment loader
226 *
227 * @param {PlaylistLoader} media the playlist to set on the segment loader
228 */
229 playlist(media) {
230 this.playlist_ = media;
231 // if we were unpaused but waiting for a playlist, start
232 // buffering now
233 if (this.sourceUpdater_ &&
234 media &&
235 this.state === 'INIT' &&
236 !this.paused()) {
237 this.state = 'READY';
238 return this.fillBuffer_();
239 }
240 }
241
242 /**
243 * Prevent the loader from fetching additional segments. If there
244 * is a segment request outstanding, it will finish processing
245 * before the loader halts. A segment loader can be unpaused by
246 * calling load().
247 */
248 pause() {
249 if (this.checkBufferTimeout_) {
250 window.clearTimeout(this.checkBufferTimeout_);
251
252 this.checkBufferTimeout_ = null;
253 }
254 }
255
256 /**
257 * Returns whether the segment loader is fetching additional
258 * segments when given the opportunity. This property can be
259 * modified through calls to pause() and load().
260 */
261 paused() {
262 return this.checkBufferTimeout_ === null;
263 }
264
265 /**
266 * setter for expired time on the SegmentLoader
267 *
268 * @param {Number} expired the exired time to set
269 */
270 expired(expired) {
271 this.expired_ = expired;
272 }
273
274 /**
275 * create/set the following mimetype on the SourceBuffer through a
276 * SourceUpdater
277 *
278 * @param {String} mimeType the mime type string to use
279 */
280 mimeType(mimeType) {
281 // TODO Allow source buffers to be re-created with different mime-types
282 if (!this.sourceUpdater_) {
283 this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, mimeType);
284 this.clearBuffer();
285
286 // if we were unpaused but waiting for a sourceUpdater, start
287 // buffering now
288 if (this.playlist_ &&
289 this.state === 'INIT' &&
290 !this.paused()) {
291 this.state = 'READY';
292 return this.fillBuffer_();
293 }
294 }
295 }
296
297 /**
298 * asynchronously/recursively monitor the buffer
299 *
300 * @private
301 */
302 monitorBuffer_() {
303 if (this.state === 'READY') {
304 this.fillBuffer_();
305 }
306 this.checkBufferTimeout_ = window.setTimeout(this.monitorBuffer_.bind(this),
307 CHECK_BUFFER_DELAY);
308 }
309
310 /**
311 * Return the amount of a segment specified by the mediaIndex overlaps
312 * the current buffered content.
313 *
314 * @param {Object} playlist the playlist object to fetch segments from
315 * @param {Number} mediaIndex the index of the segment in the playlist
316 * @param {TimeRanges} buffered the state of the buffer
317 * @returns {Number} percentage of the segment's time range that is
318 * already in `buffered`
319 */
320 getSegmentBufferedPercent_(playlist, mediaIndex, currentTime, buffered) {
321 let segment = playlist.segments[mediaIndex];
322 let startOfSegment = duration(playlist,
323 playlist.mediaSequence + mediaIndex,
324 this.expired_);
325 let segmentRange = videojs.createTimeRanges([[
326 Math.max(currentTime, startOfSegment),
327 startOfSegment + segment.duration
328 ]]);
329
330 return Ranges.calculateBufferedPercent(segmentRange, buffered);
331 }
332
333 /**
334 * Determines what segment request should be made, given current
335 * playback state.
336 *
337 * @param {TimeRanges} buffered - the state of the buffer
338 * @param {Object} playlist - the playlist object to fetch segments from
339 * @param {Number} currentTime - the playback position in seconds
340 * @returns {Object} a segment info object that describes the
341 * request that should be made or null if no request is necessary
342 */
343 checkBuffer_(buffered, playlist, currentTime) {
344 let currentBuffered = Ranges.findRange(buffered, currentTime);
345
346 // There are times when MSE reports the first segment as starting a
347 // little after 0-time so add a fudge factor to try and fix those cases
348 // or we end up fetching the same first segment over and over
349 if (currentBuffered.length === 0 && currentTime === 0) {
350 currentBuffered = Ranges.findRange(buffered,
351 currentTime + Ranges.TIME_FUDGE_FACTOR);
352 }
353
354 let bufferedTime;
355 let currentBufferedEnd;
356 let timestampOffset = this.sourceUpdater_.timestampOffset();
357 let segment;
358 let mediaIndex;
359
360 if (!playlist.segments.length) {
361 return;
362 }
363
364 if (currentBuffered.length === 0) {
365 // find the segment containing currentTime
366 mediaIndex = getMediaIndexForTime(playlist,
367 currentTime,
368 this.expired_ + this.timeCorrection_);
369 } else {
370 // find the segment adjacent to the end of the current
371 // buffered region
372 currentBufferedEnd = currentBuffered.end(0);
373 bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
374
375 // if the video has not yet played only, and we already have
376 // one segment downloaded do nothing
377 if (!this.hasPlayed_() && bufferedTime >= 1) {
378 return null;
379 }
380
381 // if there is plenty of content buffered, and the video has
382 // been played before relax for awhile
383 if (this.hasPlayed_() && bufferedTime >= GOAL_BUFFER_LENGTH) {
384 return null;
385 }
386 mediaIndex = getMediaIndexForTime(playlist,
387 currentBufferedEnd,
388 this.expired_ + this.timeCorrection_);
389 }
390
391 if (mediaIndex < 0 || mediaIndex === playlist.segments.length) {
392 return null;
393 }
394
395 // Sanity check the segment-index determining logic above but calcuating
396 // the percentage of the chosen segment that is buffered. If more than 90%
397 // of the segment is buffered then fetching it will likely not help in any
398 // way
399 let percentBuffered = this.getSegmentBufferedPercent_(playlist,
400 mediaIndex,
401 currentTime,
402 buffered);
403
404 if (percentBuffered >= 90) {
405 // Retry the buffered calculation with the next segment if there is another
406 // segment after the currently selected segment
407 if (mediaIndex + 1 < playlist.segments.length) {
408 percentBuffered = this.getSegmentBufferedPercent_(playlist,
409 mediaIndex + 1,
410 currentTime,
411 buffered);
412 }
413
414 // If both checks failed return and don't load anything
415 if (percentBuffered >= 90) {
416 return;
417 }
418
419 // Otherwise, continue with the next segment
420 mediaIndex += 1;
421 }
422
423 segment = playlist.segments[mediaIndex];
424 let startOfSegment = duration(playlist,
425 playlist.mediaSequence + mediaIndex,
426 this.expired_);
427
428 // We will need to change timestampOffset of the sourceBuffer if either of
429 // the following conditions are true:
430 // - The segment.timeline !== this.currentTimeline
431 // (we are crossing a discontinuity somehow)
432 // - The "timestampOffset" for the start of this segment is less than
433 // the currently set timestampOffset
434 if (segment.timeline !== this.currentTimeline_ ||
435 startOfSegment < this.sourceUpdater_.timestampOffset()) {
436 timestampOffset = startOfSegment;
437 }
438
439 return {
440 // resolve the segment URL relative to the playlist
441 uri: segment.resolvedUri,
442 // the segment's mediaIndex at the time it was requested
443 mediaIndex,
444 // the segment's playlist
445 playlist,
446 // unencrypted bytes of the segment
447 bytes: null,
448 // when a key is defined for this segment, the encrypted bytes
449 encryptedBytes: null,
450 // the state of the buffer before a segment is appended will be
451 // stored here so that the actual segment duration can be
452 // determined after it has been appended
453 buffered: null,
454 // The target timestampOffset for this segment when we append it
455 // to the source buffer
456 timestampOffset,
457 // The timeline that the segment is in
458 timeline: segment.timeline
459 };
460 }
461
462 /**
463 * abort all pending xhr requests and null any pending segements
464 *
465 * @private
466 */
467 abort_() {
468 if (this.xhr_) {
469 this.xhr_.abort();
470 }
471
472 // clear out the segment being processed
473 this.pendingSegment_ = null;
474 }
475
476 /**
477 * fill the buffer with segements unless the
478 * sourceBuffers are currently updating
479 *
480 * @private
481 */
482 fillBuffer_() {
483 if (this.sourceUpdater_.updating()) {
484 return;
485 }
486
487 // see if we need to begin loading immediately
488 let request = this.checkBuffer_(this.sourceUpdater_.buffered(),
489 this.playlist_,
490 this.currentTime_(),
491 this.timestampOffset_);
492
493 if (request) {
494 this.loadSegment_(request);
495 }
496 }
497
498 /**
499 * load a specific segment from a request into the buffer
500 *
501 * @private
502 */
503 loadSegment_(segmentInfo) {
504 let segment;
505 let requestTimeout;
506 let keyXhr;
507 let segmentXhr;
508 let seekable = this.seekable_();
509 let currentTime = this.currentTime_();
510 let removeToTime = 0;
511
512 // Chrome has a hard limit of 150mb of
513 // buffer and a very conservative "garbage collector"
514 // We manually clear out the old buffer to ensure
515 // we don't trigger the QuotaExceeded error
516 // on the source buffer during subsequent appends
517
518 // If we have a seekable range use that as the limit for what can be removed safely
519 // otherwise remove anything older than 1 minute before the current play head
520 if (seekable.length &&
521 seekable.start(0) > 0 &&
522 seekable.start(0) < currentTime) {
523 removeToTime = seekable.start(0);
524 } else {
525 removeToTime = currentTime - 60;
526 }
527
528 if (removeToTime > 0) {
529 this.sourceUpdater_.remove(0, removeToTime);
530 }
531
532 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
533 // Set xhr timeout to 150% of the segment duration to allow us
534 // some time to switch renditions in the event of a catastrophic
535 // decrease in network performance or a server issue.
536 requestTimeout = (segment.duration * 1.5) * 1000;
537
538 if (segment.key) {
539 keyXhr = this.hls_.xhr({
540 uri: segment.key.resolvedUri,
541 responseType: 'arraybuffer',
542 withCredentials: this.withCredentials_,
543 timeout: requestTimeout
544 }, this.handleResponse_.bind(this));
545 }
546 this.pendingSegment_ = segmentInfo;
547 segmentXhr = this.hls_.xhr({
548 uri: segmentInfo.uri,
549 responseType: 'arraybuffer',
550 withCredentials: this.withCredentials_,
551 timeout: requestTimeout,
552 headers: segmentXhrHeaders(segment)
553 }, this.handleResponse_.bind(this));
554
555 this.xhr_ = {
556 keyXhr,
557 segmentXhr,
558 abort() {
559 if (this.segmentXhr) {
560 // Prevent error handler from running.
561 this.segmentXhr.onreadystatechange = null;
562 this.segmentXhr.abort();
563 this.segmentXhr = null;
564 }
565 if (this.keyXhr) {
566 // Prevent error handler from running.
567 this.keyXhr.onreadystatechange = null;
568 this.keyXhr.abort();
569 this.keyXhr = null;
570 }
571 }
572 };
573
574 this.state = 'WAITING';
575 }
576
577 /**
578 * triggered when a segment response is received
579 *
580 * @private
581 */
582 handleResponse_(error, request) {
583 let segmentInfo;
584 let segment;
585 let keyXhrRequest;
586 let view;
587
588 // timeout of previously aborted request
589 if (!this.xhr_ ||
590 (request !== this.xhr_.segmentXhr && request !== this.xhr_.keyXhr)) {
591 return;
592 }
593
594 segmentInfo = this.pendingSegment_;
595 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
596
597 // if a request times out, reset bandwidth tracking
598 if (request.timedout) {
599 this.abort_();
600 this.bandwidth = 1;
601 this.roundTrip = NaN;
602 this.state = 'READY';
603 return this.trigger('progress');
604 }
605
606 // trigger an event for other errors
607 if (!request.aborted && error) {
608 // abort will clear xhr_
609 keyXhrRequest = this.xhr_.keyXhr;
610 this.abort_();
611 this.error({
612 status: request.status,
613 message: request === keyXhrRequest ?
614 'HLS key request error at URL: ' + segment.key.uri :
615 'HLS segment request error at URL: ' + segmentInfo.uri,
616 code: 2,
617 xhr: request
618 });
619 this.state = 'READY';
620 this.pause();
621 return this.trigger('error');
622 }
623
624 // stop processing if the request was aborted
625 if (!request.response) {
626 this.abort_();
627 return;
628 }
629
630 if (request === this.xhr_.segmentXhr) {
631 // the segment request is no longer outstanding
632 this.xhr_.segmentXhr = null;
633
634 // calculate the download bandwidth based on segment request
635 this.roundTrip = request.roundTripTime;
636 this.bandwidth = request.bandwidth;
637 this.bytesReceived += request.bytesReceived || 0;
638
639 if (segment.key) {
640 segmentInfo.encryptedBytes = new Uint8Array(request.response);
641 } else {
642 segmentInfo.bytes = new Uint8Array(request.response);
643 }
644 }
645
646 if (request === this.xhr_.keyXhr) {
647 keyXhrRequest = this.xhr_.segmentXhr;
648 // the key request is no longer outstanding
649 this.xhr_.keyXhr = null;
650
651 if (request.response.byteLength !== 16) {
652 this.abort_();
653 this.error({
654 status: request.status,
655 message: 'Invalid HLS key at URL: ' + segment.key.uri,
656 code: 2,
657 xhr: request
658 });
659 this.state = 'READY';
660 this.pause();
661 return this.trigger('error');
662 }
663
664 view = new DataView(request.response);
665 segment.key.bytes = new Uint32Array([
666 view.getUint32(0),
667 view.getUint32(4),
668 view.getUint32(8),
669 view.getUint32(12)
670 ]);
671
672 // if the media sequence is greater than 2^32, the IV will be incorrect
673 // assuming 10s segments, that would be about 1300 years
674 segment.key.iv = segment.key.iv || new Uint32Array([
675 0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence
676 ]);
677 }
678
679 if (!this.xhr_.segmentXhr && !this.xhr_.keyXhr) {
680 this.xhr_ = null;
681 this.processResponse_();
682 }
683 }
684
685 /**
686 * clear anything that is currently in the buffer and throw it away
687 */
688 clearBuffer() {
689 if (this.sourceUpdater_ &&
690 this.sourceUpdater_.buffered().length) {
691 this.sourceUpdater_.remove(0, Infinity);
692 }
693 }
694
695 /**
696 * Decrypt the segment that is being loaded if necessary
697 *
698 * @private
699 */
700 processResponse_() {
701 let segmentInfo;
702 let segment;
703
704 this.state = 'DECRYPTING';
705
706 segmentInfo = this.pendingSegment_;
707 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
708
709 if (segment.key) {
710 // this is an encrypted segment
711 // incrementally decrypt the segment
712 /* eslint-disable no-new, handle-callback-err */
713 new Decrypter(segmentInfo.encryptedBytes,
714 segment.key.bytes,
715 segment.key.iv,
716 (function(err, bytes) {
717 // err always null
718 segmentInfo.bytes = bytes;
719 this.handleSegment_();
720 }).bind(this));
721 /* eslint-enable */
722 } else {
723 this.handleSegment_();
724 }
725 }
726
727 /**
728 * append a decrypted segement to the SourceBuffer through a SourceUpdater
729 *
730 * @private
731 */
732 handleSegment_() {
733 let segmentInfo;
734
735 this.state = 'APPENDING';
736 segmentInfo = this.pendingSegment_;
737 segmentInfo.buffered = this.sourceUpdater_.buffered();
738 this.currentTimeline_ = segmentInfo.timeline;
739
740 if (segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
741 this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
742 }
743
744 this.sourceUpdater_.appendBuffer(segmentInfo.bytes,
745 this.handleUpdateEnd_.bind(this));
746 }
747
748 /**
749 * callback to run when appendBuffer is finished. detects if we are
750 * in a good state to do things with the data we got, or if we need
751 * to wait for more
752 *
753 * @private
754 */
755 handleUpdateEnd_() {
756 let segmentInfo = this.pendingSegment_;
757 let currentTime = this.currentTime_();
758
759 this.pendingSegment_ = null;
760 // add segment timeline information if we're still using the
761 // same playlist
762 if (segmentInfo && segmentInfo.playlist.uri === this.playlist_.uri) {
763 this.updateTimeline_(segmentInfo);
764 this.trigger('progress');
765 }
766
767 let currentMediaIndex = segmentInfo.mediaIndex;
768
769 currentMediaIndex +=
770 segmentInfo.playlist.mediaSequence - this.playlist_.mediaSequence;
771
772 let currentBuffered = Ranges.findRange(this.sourceUpdater_.buffered(), currentTime);
773
774 // any time an update finishes and the last segment is in the
775 // buffer, end the stream. this ensures the "ended" event will
776 // fire if playback reaches that point.
777 let isEndOfStream = detectEndOfStream(segmentInfo.playlist,
778 this.mediaSource_,
779 currentMediaIndex,
780 currentBuffered);
781
782 if (isEndOfStream) {
783 this.mediaSource_.endOfStream();
784 }
785
786 // when seeking to the beginning of the seekable range, it's
787 // possible that imprecise timing information may cause the seek to
788 // end up earlier than the start of the range
789 // in that case, seek again
790 let seekable = this.seekable_();
791 let next = Ranges.findNextRange(this.sourceUpdater_.buffered(), currentTime);
792
793 if (this.seeking_() &&
794 currentBuffered.length === 0) {
795 if (seekable.length &&
796 currentTime < seekable.start(0)) {
797
798 if (next.length) {
799 videojs.log('tried seeking to', currentTime,
800 'but that was too early, retrying at', next.start(0));
801 this.setCurrentTime_(next.start(0) + Ranges.TIME_FUDGE_FACTOR);
802 }
803 }
804 }
805
806 this.state = 'READY';
807
808 if (!this.paused()) {
809 this.fillBuffer_();
810 }
811 }
812
813 /**
814 * annotate the segment with any start and end time information
815 * added by the media processing
816 *
817 * @private
818 * @param {Object} segmentInfo annotate a segment with time info
819 */
820 updateTimeline_(segmentInfo) {
821 let segment;
822 let timelineUpdate;
823 let playlist = segmentInfo.playlist;
824 let currentMediaIndex = segmentInfo.mediaIndex;
825
826 currentMediaIndex += playlist.mediaSequence - this.playlist_.mediaSequence;
827 segment = playlist.segments[currentMediaIndex];
828
829 if (!segment) {
830 return;
831 }
832
833 timelineUpdate = Ranges.findSoleUncommonTimeRangesEnd(segmentInfo.buffered,
834 this.sourceUpdater_.buffered());
835
836 // Update segment meta-data (duration and end-point) based on timeline
837 let timelineUpdated = updateSegmentMetadata(playlist,
838 currentMediaIndex,
839 timelineUpdate);
840
841 // the last segment append must have been entirely in the
842 // already buffered time ranges. adjust the timeCorrection
843 // offset to fetch forward until we find a segment that adds
844 // to the buffered time ranges and improves subsequent media
845 // index calculations.
846 if (!timelineUpdated) {
847 this.timeCorrection_ -= segment.duration;
848 } else {
849 this.timeCorrection_ = 0;
850 }
851 }
852 }
1 /**
2 * @file source-updater.js
3 */
4 import videojs from 'video.js';
5
6 /**
7 * A queue of callbacks to be serialized and applied when a
8 * MediaSource and its associated SourceBuffers are not in the
9 * updating state. It is used by the segment loader to update the
10 * underlying SourceBuffers when new data is loaded, for instance.
11 *
12 * @class SourceUpdater
13 * @param {MediaSource} mediaSource the MediaSource to create the
14 * SourceBuffer from
15 * @param {String} mimeType the desired MIME type of the underlying
16 * SourceBuffer
17 */
18 export default class SourceUpdater {
19 constructor(mediaSource, mimeType) {
20 let createSourceBuffer = () => {
21 this.sourceBuffer_ = mediaSource.addSourceBuffer(mimeType);
22
23 // run completion handlers and process callbacks as updateend
24 // events fire
25 this.sourceBuffer_.addEventListener('updateend', () => {
26 let pendingCallback = this.pendingCallback_;
27
28 this.pendingCallback_ = null;
29
30 if (pendingCallback) {
31 pendingCallback();
32 }
33 });
34 this.sourceBuffer_.addEventListener('updateend',
35 this.runCallback_.bind(this));
36
37 this.runCallback_();
38 };
39
40 this.callbacks_ = [];
41 this.pendingCallback_ = null;
42 this.timestampOffset_ = 0;
43 this.mediaSource = mediaSource;
44
45 if (mediaSource.readyState === 'closed') {
46 mediaSource.addEventListener('sourceopen', createSourceBuffer);
47 } else {
48 createSourceBuffer();
49 }
50 }
51
52 /**
53 * Aborts the current segment and resets the segment parser.
54 *
55 * @param {Function} done function to call when done
56 * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void
57 */
58 abort(done) {
59 this.queueCallback_(() => {
60 this.sourceBuffer_.abort();
61 }, done);
62 }
63
64 /**
65 * Queue an update to append an ArrayBuffer.
66 *
67 * @param {ArrayBuffer} bytes
68 * @param {Function} done the function to call when done
69 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data
70 */
71 appendBuffer(bytes, done) {
72 this.queueCallback_(() => {
73 this.sourceBuffer_.appendBuffer(bytes);
74 }, done);
75 }
76
77 /**
78 * Indicates what TimeRanges are buffered in the managed SourceBuffer.
79 *
80 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered
81 */
82 buffered() {
83 if (!this.sourceBuffer_) {
84 return videojs.createTimeRanges();
85 }
86 return this.sourceBuffer_.buffered;
87 }
88
89 /**
90 * Queue an update to set the duration.
91 *
92 * @param {Double} duration what to set the duration to
93 * @see http://www.w3.org/TR/media-source/#widl-MediaSource-duration
94 */
95 duration(duration) {
96 this.queueCallback_(() => {
97 this.sourceBuffer_.duration = duration;
98 });
99 }
100
101 /**
102 * Queue an update to remove a time range from the buffer.
103 *
104 * @param {Number} start where to start the removal
105 * @param {Number} end where to end the removal
106 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
107 */
108 remove(start, end) {
109 this.queueCallback_(() => {
110 this.sourceBuffer_.remove(start, end);
111 });
112 }
113
114 /**
115 * wether the underlying sourceBuffer is updating or not
116 *
117 * @return {Boolean} the updating status of the SourceBuffer
118 */
119 updating() {
120 return !this.sourceBuffer_ || this.sourceBuffer_.updating;
121 }
122
123 /**
124 * Set/get the timestampoffset on the SourceBuffer
125 *
126 * @return {Number} the timestamp offset
127 */
128 timestampOffset(offset) {
129 if (typeof offset !== 'undefined') {
130 this.queueCallback_(() => {
131 this.sourceBuffer_.timestampOffset = offset;
132 });
133 this.timestampOffset_ = offset;
134 }
135 return this.timestampOffset_;
136 }
137
138 /**
139 * que a callback to run
140 */
141 queueCallback_(callback, done) {
142 this.callbacks_.push([callback.bind(this), done]);
143 this.runCallback_();
144 }
145
146 /**
147 * run a queued callback
148 */
149 runCallback_() {
150 let callbacks;
151
152 if (this.sourceBuffer_ &&
153 !this.sourceBuffer_.updating &&
154 this.callbacks_.length) {
155 callbacks = this.callbacks_.shift();
156 this.pendingCallback_ = callbacks[1];
157 callbacks[0]();
158 }
159 }
160
161 /**
162 * dispose of the source updater and the underlying sourceBuffer
163 */
164 dispose() {
165 if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') {
166 this.sourceBuffer_.abort();
167 }
168 }
169 }
1 /** 1 /**
2 * @file stream.js
3 */
4 /**
2 * A lightweight readable stream implemention that handles event dispatching. 5 * A lightweight readable stream implemention that handles event dispatching.
6 *
7 * @class Stream
3 */ 8 */
4 export default class Stream { 9 export default class Stream {
5 constructor() { 10 constructor() {
...@@ -8,8 +13,9 @@ export default class Stream { ...@@ -8,8 +13,9 @@ export default class Stream {
8 13
9 /** 14 /**
10 * Add a listener for a specified event type. 15 * Add a listener for a specified event type.
11 * @param type {string} the event name 16 *
12 * @param listener {function} the callback to be invoked when an event of 17 * @param {String} type the event name
18 * @param {Function} listener the callback to be invoked when an event of
13 * the specified type occurs 19 * the specified type occurs
14 */ 20 */
15 on(type, listener) { 21 on(type, listener) {
...@@ -21,9 +27,11 @@ export default class Stream { ...@@ -21,9 +27,11 @@ export default class Stream {
21 27
22 /** 28 /**
23 * Remove a listener for a specified event type. 29 * Remove a listener for a specified event type.
24 * @param type {string} the event name 30 *
25 * @param listener {function} a function previously registered for this 31 * @param {String} type the event name
32 * @param {Function} listener a function previously registered for this
26 * type of event through `on` 33 * type of event through `on`
34 * @return {Boolean} if we could turn it off or not
27 */ 35 */
28 off(type, listener) { 36 off(type, listener) {
29 let index; 37 let index;
...@@ -39,7 +47,8 @@ export default class Stream { ...@@ -39,7 +47,8 @@ export default class Stream {
39 /** 47 /**
40 * Trigger an event of the specified type on this stream. Any additional 48 * Trigger an event of the specified type on this stream. Any additional
41 * arguments to this function are passed as parameters to event listeners. 49 * arguments to this function are passed as parameters to event listeners.
42 * @param type {string} the event name 50 *
51 * @param {String} type the event name
43 */ 52 */
44 trigger(type) { 53 trigger(type) {
45 let callbacks; 54 let callbacks;
...@@ -79,7 +88,8 @@ export default class Stream { ...@@ -79,7 +88,8 @@ export default class Stream {
79 * Forwards all `data` events on this stream to the destination stream. The 88 * Forwards all `data` events on this stream to the destination stream. The
80 * destination stream should provide a method `push` to receive the data 89 * destination stream should provide a method `push` to receive the data
81 * events as they arrive. 90 * events as they arrive.
82 * @param destination {stream} the stream that will receive all `data` events 91 *
92 * @param {Stream} destination the stream that will receive all `data` events
83 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 93 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
84 */ 94 */
85 pipe(destination) { 95 pipe(destination) {
......
1 /** 1 /**
2 * videojs-hls 2 * @file videojs-contrib-hls.js
3 *
3 * The main file for the HLS project. 4 * The main file for the HLS project.
4 * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE 5 * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE
5 */ 6 */
7 import document from 'global/document';
6 import PlaylistLoader from './playlist-loader'; 8 import PlaylistLoader from './playlist-loader';
7 import Playlist from './playlist'; 9 import Playlist from './playlist';
8 import xhrFactory from './xhr'; 10 import xhrFactory from './xhr';
...@@ -11,7 +13,34 @@ import utils from './bin-utils'; ...@@ -11,7 +13,34 @@ import utils from './bin-utils';
11 import {MediaSource, URL} from 'videojs-contrib-media-sources'; 13 import {MediaSource, URL} from 'videojs-contrib-media-sources';
12 import m3u8 from './m3u8'; 14 import m3u8 from './m3u8';
13 import videojs from 'video.js'; 15 import videojs from 'video.js';
14 import resolveUrl from './resolve-url'; 16 import MasterPlaylistController from './master-playlist-controller';
17
18 /**
19 * determine if an object a is differnt from
20 * and object b. both only having one dimensional
21 * properties
22 *
23 * @param {Object} a object one
24 * @param {Object} b object two
25 * @return {Boolean} if the object has changed or not
26 */
27 const objectChanged = function(a, b) {
28 if (typeof a !== typeof b) {
29 return true;
30 }
31 // if we have a different number of elements
32 // something has changed
33 if (Object.keys(a).length !== Object.keys(b).length) {
34 return true;
35 }
36
37 for (let prop in a) {
38 if (!b[prop] || a[prop] !== b[prop]) {
39 return true;
40 }
41 }
42 return false;
43 };
15 44
16 const Hls = { 45 const Hls = {
17 PlaylistLoader, 46 PlaylistLoader,
...@@ -26,183 +55,19 @@ const Hls = { ...@@ -26,183 +55,19 @@ const Hls = {
26 // the desired length of video to maintain in the buffer, in seconds 55 // the desired length of video to maintain in the buffer, in seconds
27 Hls.GOAL_BUFFER_LENGTH = 30; 56 Hls.GOAL_BUFFER_LENGTH = 30;
28 57
29 // HLS is a source handler, not a tech. Make sure attempts to use it
30 // as one do not cause exceptions.
31 Hls.canPlaySource = function() {
32 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
33 'your player\'s techOrder.');
34 };
35
36 // Search for a likely end time for the segment that was just appened
37 // based on the state of the `buffered` property before and after the
38 // append.
39 // If we found only one such uncommon end-point return it.
40 Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
41 let i;
42 let start;
43 let end;
44 let result = [];
45 let edges = [];
46
47 // In order to qualify as a possible candidate, the end point must:
48 // 1) Not have already existed in the `original` ranges
49 // 2) Not result from the shrinking of a range that already existed
50 // in the `original` ranges
51 // 3) Not be contained inside of a range that existed in `original`
52 let overlapsCurrentEnd = function(span) {
53 return (span[0] <= end && span[1] >= end);
54 };
55
56 if (original) {
57 // Save all the edges in the `original` TimeRanges object
58 for (i = 0; i < original.length; i++) {
59 start = original.start(i);
60 end = original.end(i);
61
62 edges.push([start, end]);
63 }
64 }
65
66 if (update) {
67 // Save any end-points in `update` that are not in the `original`
68 // TimeRanges object
69 for (i = 0; i < update.length; i++) {
70 start = update.start(i);
71 end = update.end(i);
72
73 if (edges.some(overlapsCurrentEnd)) {
74 continue;
75 }
76
77 // at this point it must be a unique non-shrinking end edge
78 result.push(end);
79 }
80 }
81
82 // we err on the side of caution and return null if didn't find
83 // exactly *one* differing end edge in the search above
84 if (result.length !== 1) {
85 return null;
86 }
87
88 return result[0];
89 };
90
91 /**
92 * Whether the browser has built-in HLS support.
93 */
94 Hls.supportsNativeHls = (function() {
95 let video = document.createElement('video');
96 let xMpegUrl;
97 let vndMpeg;
98
99 // native HLS is definitely not supported if HTML5 video isn't
100 if (!videojs.getComponent('Html5').isSupported()) {
101 return false;
102 }
103
104 xMpegUrl = video.canPlayType('application/x-mpegURL');
105 vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
106 return (/probably|maybe/).test(xMpegUrl) ||
107 (/probably|maybe/).test(vndMpeg);
108 }());
109
110 // HLS is a source handler, not a tech. Make sure attempts to use it
111 // as one do not cause exceptions.
112 Hls.isSupported = function() {
113 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
114 'your player\'s techOrder.');
115 };
116
117 /**
118 * A comparator function to sort two playlist object by bandwidth.
119 * @param left {object} a media playlist object
120 * @param right {object} a media playlist object
121 * @return {number} Greater than zero if the bandwidth attribute of
122 * left is greater than the corresponding attribute of right. Less
123 * than zero if the bandwidth of right is greater than left and
124 * exactly zero if the two are equal.
125 */
126 Hls.comparePlaylistBandwidth = function(left, right) {
127 let leftBandwidth;
128 let rightBandwidth;
129
130 if (left.attributes && left.attributes.BANDWIDTH) {
131 leftBandwidth = left.attributes.BANDWIDTH;
132 }
133 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
134 if (right.attributes && right.attributes.BANDWIDTH) {
135 rightBandwidth = right.attributes.BANDWIDTH;
136 }
137 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
138
139 return leftBandwidth - rightBandwidth;
140 };
141
142 /**
143 * A comparator function to sort two playlist object by resolution (width).
144 * @param left {object} a media playlist object
145 * @param right {object} a media playlist object
146 * @return {number} Greater than zero if the resolution.width attribute of
147 * left is greater than the corresponding attribute of right. Less
148 * than zero if the resolution.width of right is greater than left and
149 * exactly zero if the two are equal.
150 */
151 Hls.comparePlaylistResolution = function(left, right) {
152 let leftWidth;
153 let rightWidth;
154
155 if (left.attributes &&
156 left.attributes.RESOLUTION &&
157 left.attributes.RESOLUTION.width) {
158 leftWidth = left.attributes.RESOLUTION.width;
159 }
160
161 leftWidth = leftWidth || window.Number.MAX_VALUE;
162
163 if (right.attributes &&
164 right.attributes.RESOLUTION &&
165 right.attributes.RESOLUTION.width) {
166 rightWidth = right.attributes.RESOLUTION.width;
167 }
168
169 rightWidth = rightWidth || window.Number.MAX_VALUE;
170
171 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
172 // have the same media dimensions/ resolution
173 if (leftWidth === rightWidth &&
174 left.attributes.BANDWIDTH &&
175 right.attributes.BANDWIDTH) {
176 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
177 }
178 return leftWidth - rightWidth;
179 };
180
181 // A fudge factor to apply to advertised playlist bitrates to account for 58 // A fudge factor to apply to advertised playlist bitrates to account for
182 // temporary flucations in client bandwidth 59 // temporary flucations in client bandwidth
183 const bandwidthVariance = 1.2; 60 const BANDWIDTH_VARIANCE = 1.2;
184
185 // 5 minute blacklist
186 const blacklistDuration = 5 * 60 * 1000;
187
188 // Fudge factor to account for TimeRanges rounding
189 const TIME_FUDGE_FACTOR = 1 / 30;
190 const Component = videojs.getComponent('Component');
191
192 // The amount of time to wait between checking the state of the buffer
193 const bufferCheckInterval = 500;
194
195 // returns true if a key has failed to download within a certain amount of retries
196 const keyFailed = function(key) {
197 return key.retries && key.retries >= 2;
198 };
199 61
200 /** 62 /**
201 * Returns the CSS value for the specified property on an element 63 * Returns the CSS value for the specified property on an element
202 * using `getComputedStyle`. Firefox has a long-standing issue where 64 * using `getComputedStyle`. Firefox has a long-standing issue where
203 * getComputedStyle() may return null when running in an iframe with 65 * getComputedStyle() may return null when running in an iframe with
204 * `display: none`. 66 * `display: none`.
67 *
205 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 68 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
69 * @param {HTMLElement} el the htmlelement to work on
70 * @param {string} the proprety to get the style for
206 */ 71 */
207 const safeGetComputedStyle = function(el, property) { 72 const safeGetComputedStyle = function(el, property) {
208 let result; 73 let result;
...@@ -220,137 +85,195 @@ const safeGetComputedStyle = function(el, property) { ...@@ -220,137 +85,195 @@ const safeGetComputedStyle = function(el, property) {
220 }; 85 };
221 86
222 /** 87 /**
223 * Updates segment with information about its end-point in time and, optionally, 88 * Chooses the appropriate media playlist based on the current
224 * the segment duration if we have enough information to determine a segment duration 89 * bandwidth estimate and the player size.
225 * accurately. 90 *
226 * @param playlist {object} a media playlist object 91 * @return {Playlist} the highest bitrate playlist less than the currently detected
227 * @param segmentIndex {number} the index of segment we last appended 92 * bandwidth, accounting for some amount of bandwidth variance
228 * @param segmentEnd {number} the known of the segment referenced by segmentIndex
229 */ 93 */
230 const updateSegmentMetadata = function(playlist, segmentIndex, segmentEnd) { 94 Hls.STANDARD_PLAYLIST_SELECTOR = function() {
231 if (!playlist) { 95 let effectiveBitrate;
232 return; 96 let sortedPlaylists = this.playlists.master.playlists.slice();
233 } 97 let bandwidthPlaylists = [];
98 let now = +new Date();
99 let i;
100 let variant;
101 let bandwidthBestVariant;
102 let resolutionPlusOne;
103 let resolutionPlusOneAttribute;
104 let resolutionBestVariant;
105 let width;
106 let height;
107
108 sortedPlaylists.sort(Hls.comparePlaylistBandwidth);
109
110 // filter out any playlists that have been excluded due to
111 // incompatible configurations or playback errors
112 sortedPlaylists = sortedPlaylists.filter((localVariant) => {
113 if (typeof localVariant.excludeUntil !== 'undefined') {
114 return now >= localVariant.excludeUntil;
115 }
116 return true;
117 });
118
119 // filter out any variant that has greater effective bitrate
120 // than the current estimated bandwidth
121 i = sortedPlaylists.length;
122 while (i--) {
123 variant = sortedPlaylists[i];
124
125 // ignore playlists without bandwidth information
126 if (!variant.attributes || !variant.attributes.BANDWIDTH) {
127 continue;
128 }
234 129
235 let segment = playlist.segments[segmentIndex]; 130 effectiveBitrate = variant.attributes.BANDWIDTH * BANDWIDTH_VARIANCE;
236 let previousSegment = playlist.segments[segmentIndex - 1];
237 131
238 if (segmentEnd && segment) { 132 if (effectiveBitrate < this.bandwidth) {
239 segment.end = segmentEnd; 133 bandwidthPlaylists.push(variant);
240 134
241 // fix up segment durations based on segment end data 135 // since the playlists are sorted in ascending order by
242 if (!previousSegment) { 136 // bandwidth, the first viable variant is the best
243 // first segment is always has a start time of 0 making its duration 137 if (!bandwidthBestVariant) {
244 // equal to the segment end 138 bandwidthBestVariant = variant;
245 segment.duration = segment.end; 139 }
246 } else if (previousSegment.end) {
247 segment.duration = segment.end - previousSegment.end;
248 } 140 }
249 } 141 }
250 };
251 142
252 /** 143 i = bandwidthPlaylists.length;
253 * Determines if we should call endOfStream on the media source based on the state
254 * of the buffer or if appened segment was the final segment in the playlist.
255 * @param playlist {object} a media playlist object
256 * @param mediaSource {object} the MediaSource object
257 * @param segmentIndex {number} the index of segment we last appended
258 * @param currentBuffered {object} the buffered region that currentTime resides in
259 * @return {boolean} whether the calling function should call endOfStream on the MediaSource
260 */
261 const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentBuffered) {
262 if (!playlist) {
263 return false;
264 }
265 144
266 let segments = playlist.segments; 145 // sort variants by resolution
146 bandwidthPlaylists.sort(Hls.comparePlaylistResolution);
267 147
268 // determine a few boolean values to help make the branch below easier 148 // forget our old variant from above,
269 // to read 149 // or we might choose that in high-bandwidth scenarios
270 let appendedLastSegment = (segmentIndex === segments.length - 1); 150 // (this could be the lowest bitrate rendition as we go through all of them above)
271 let bufferedToEnd = (currentBuffered.length && 151 variant = null;
272 segments[segments.length - 1].end <= currentBuffered.end(0));
273 152
274 // if we've buffered to the end of the video, we need to call endOfStream 153 width = parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10);
275 // so that MediaSources can trigger the `ended` event when it runs out of 154 height = parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10);
276 // buffered data instead of waiting for me
277 return playlist.endList &&
278 mediaSource.readyState === 'open' &&
279 (appendedLastSegment || bufferedToEnd);
280 };
281 155
282 const parseCodecs = function(codecs) { 156 // iterate through the bandwidth-filtered playlists and find
283 let result = { 157 // best rendition by player dimension
284 codecCount: 0, 158 while (i--) {
285 videoCodec: null, 159 variant = bandwidthPlaylists[i];
286 audioProfile: null
287 };
288 160
289 result.codecCount = codecs.split(',').length; 161 // ignore playlists without resolution information
290 result.codecCount = result.codecCount || 2; 162 if (!variant.attributes ||
163 !variant.attributes.RESOLUTION ||
164 !variant.attributes.RESOLUTION.width ||
165 !variant.attributes.RESOLUTION.height) {
166 continue;
167 }
168
169 // since the playlists are sorted, the first variant that has
170 // dimensions less than or equal to the player size is the best
171 let variantResolution = variant.attributes.RESOLUTION;
291 172
292 // parse the video codec but ignore the version 173 if (variantResolution.width === width &&
293 result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs); 174 variantResolution.height === height) {
294 result.videoCodec = result.videoCodec && result.videoCodec[2]; 175 // if we have the exact resolution as the player use it
176 resolutionPlusOne = null;
177 resolutionBestVariant = variant;
178 break;
179 } else if (variantResolution.width < width &&
180 variantResolution.height < height) {
181 // if both dimensions are less than the player use the
182 // previous (next-largest) variant
183 break;
184 } else if (!resolutionPlusOne ||
185 (variantResolution.width < resolutionPlusOneAttribute.width &&
186 variantResolution.height < resolutionPlusOneAttribute.height)) {
187 // If we still haven't found a good match keep a
188 // reference to the previous variant for the next loop
189 // iteration
295 190
296 // parse the last field of the audio codec 191 // By only saving variants if they are smaller than the
297 result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs); 192 // previously saved variant, we ensure that we also pick
298 result.audioProfile = result.audioProfile && result.audioProfile[2]; 193 // the highest bandwidth variant that is just-larger-than
194 // the video player
195 resolutionPlusOne = variant;
196 resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION;
197 }
198 }
299 199
300 return result; 200 // fallback chain of variants
201 return resolutionPlusOne ||
202 resolutionBestVariant ||
203 bandwidthBestVariant ||
204 sortedPlaylists[0];
301 }; 205 };
302 206
303 const filterBufferedRanges = function(predicate) { 207 // HLS is a source handler, not a tech. Make sure attempts to use it
304 return function(time) { 208 // as one do not cause exceptions.
305 let i; 209 Hls.canPlaySource = function() {
306 let ranges = []; 210 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
307 let tech = this.tech_; 211 'your player\'s techOrder.');
308 212 };
309 // !!The order of the next two assignments is important!!
310 // `currentTime` must be equal-to or greater-than the start of the
311 // buffered range. Flash executes out-of-process so, every value can
312 // change behind the scenes from line-to-line. By reading `currentTime`
313 // after `buffered`, we ensure that it is always a current or later
314 // value during playback.
315 let buffered = tech.buffered();
316
317 if (typeof time === 'undefined') {
318 time = tech.currentTime();
319 }
320 213
321 // IE 11 has a bug where it will report a the video as fully buffered 214 /**
322 // before any data has been loaded. This is a work around where we 215 * Whether the browser has built-in HLS support.
323 // report a fully empty buffer until SourceBuffers have been created 216 */
324 // which is after a segment has been loaded and transmuxed. 217 Hls.supportsNativeHls = (function() {
325 if (!this.mediaSource || 218 let video = document.createElement('video');
326 (this.mediaSource.mediaSource_ &&
327 !this.mediaSource.mediaSource_.sourceBuffers.length)) {
328 return videojs.createTimeRanges([]);
329 }
330 219
331 if (buffered && buffered.length) { 220 // native HLS is definitely not supported if HTML5 video isn't
332 // Search for a range containing the play-head 221 if (!videojs.getComponent('Html5').isSupported()) {
333 for (i = 0; i < buffered.length; i++) { 222 return false;
334 if (predicate(buffered.start(i), buffered.end(i), time)) { 223 }
335 ranges.push([buffered.start(i), buffered.end(i)]);
336 }
337 }
338 }
339 224
340 return videojs.createTimeRanges(ranges); 225 // HLS manifests can go by many mime-types
341 }; 226 let canPlay = [
227 // Apple santioned
228 'application/vnd.apple.mpegurl',
229 // Apple sanctioned for backwards compatibility
230 'audio/mpegurl',
231 // Very common
232 'audio/x-mpegurl',
233 // Very common
234 'application/x-mpegurl',
235 // Included for completeness
236 'video/x-mpegurl',
237 'video/mpegurl',
238 'application/mpegurl'
239 ];
240
241 return canPlay.some(function(canItPlay) {
242 return (/maybe|probably/i).test(video.canPlayType(canItPlay));
243 });
244 }());
245
246 /**
247 * HLS is a source handler, not a tech. Make sure attempts to use it
248 * as one do not cause exceptions.
249 */
250 Hls.isSupported = function() {
251 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
252 'your player\'s techOrder.');
342 }; 253 };
343 254
344 export default class HlsHandler extends Component { 255 const Component = videojs.getComponent('Component');
345 constructor(tech, options) { 256
257 /**
258 * The Hls Handler object, where we orchestrate all of the parts
259 * of HLS to interact with video.js
260 *
261 * @class HlsHandler
262 * @extends videojs.Component
263 * @param {Object} source the soruce object
264 * @param {Tech} tech the parent tech object
265 * @param {Object} options optional and required options
266 */
267 class HlsHandler extends Component {
268 constructor(source, tech, options) {
346 super(tech); 269 super(tech);
347 let _player;
348 270
349 // tech.player() is deprecated but setup a reference to HLS for 271 // tech.player() is deprecated but setup a reference to HLS for
350 // backwards-compatibility 272 // backwards-compatibility
351 if (tech.options_ && tech.options_.playerId) { 273 if (tech.options_ && tech.options_.playerId) {
352 _player = videojs(tech.options_.playerId); 274 let _player = videojs(tech.options_.playerId);
353 if (!_player.hls) { 275
276 if (!_player.hasOwnProperty('hls')) {
354 Object.defineProperty(_player, 'hls', { 277 Object.defineProperty(_player, 'hls', {
355 get: () => { 278 get: () => {
356 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.'); 279 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
...@@ -359,1188 +282,228 @@ export default class HlsHandler extends Component { ...@@ -359,1188 +282,228 @@ export default class HlsHandler extends Component {
359 }); 282 });
360 } 283 }
361 } 284 }
285
286 this.options_ = videojs.mergeOptions(videojs.options.hls || {}, options.hls);
362 this.tech_ = tech; 287 this.tech_ = tech;
363 this.source_ = options.source; 288 this.source_ = source;
364 this.mode_ = options.mode;
365 // the segment info object for a segment that is in the process of
366 // being downloaded or processed
367 this.pendingSegment_ = null;
368 289
369 // start playlist selection at a reasonable bandwidth for 290 // start playlist selection at a reasonable bandwidth for
370 // broadband internet 291 // broadband internet
371 // 0.5 Mbps 292 // 0.5 Mbps
372 this.bandwidth = options.bandwidth || 4194304; 293 this.bandwidth = this.options_.bandwidth || 4194304;
373 this.bytesReceived = 0; 294 this.bytesReceived = 0;
374 295
375 // loadingState_ tracks how far along the buffering process we 296 // listen for fullscreenchange events for this player so that we
376 // have been given permission to proceed. There are three possible 297 // can adjust our quality selection quickly
377 // values: 298 this.on(document, [
378 // - none: do not load playlists or segments 299 'fullscreenchange', 'webkitfullscreenchange',
379 // - meta: load playlists but not segments 300 'mozfullscreenchange', 'MSFullscreenChange'
380 // - segments: load everything 301 ], (event) => {
381 this.loadingState_ = 'none'; 302 let fullscreenElement = document.fullscreenElement ||
382 if (this.tech_.preload() !== 'none') { 303 document.webkitFullscreenElement ||
383 this.loadingState_ = 'meta'; 304 document.mozFullScreenElement ||
384 } 305 document.msFullscreenElement;
385 306
386 // periodically check if new data needs to be downloaded or 307 if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
387 // buffered data should be appended to the source buffer 308 this.masterPlaylistController_.fastQualityChange_();
388 this.startCheckingBuffer_(); 309 }
310 });
389 311
390 this.on(this.tech_, 'seeking', function() { 312 this.on(this.tech_, 'seeking', function() {
391 this.setCurrentTime(this.tech_.currentTime()); 313 this.setCurrentTime(this.tech_.currentTime());
392 }); 314 });
393 this.on(this.tech_, 'error', function() { 315 this.on(this.tech_, 'error', function() {
394 this.stopCheckingBuffer_(); 316 if (this.masterPlaylistController_) {
317 this.masterPlaylistController_.pauseLoading();
318 }
395 }); 319 });
396 320
321 this.audioTrackChange_ = () => {
322 this.masterPlaylistController_.useAudio();
323 };
324
397 this.on(this.tech_, 'play', this.play); 325 this.on(this.tech_, 'play', this.play);
398 } 326 }
399 src(src) {
400 let oldMediaPlaylist;
401 327
328 /**
329 * called when player.src gets called, handle a new source
330 *
331 * @param {Object} src the source object to handle
332 */
333 src(src) {
402 // do nothing if the src is falsey 334 // do nothing if the src is falsey
403 if (!src) { 335 if (!src) {
404 return; 336 return;
405 } 337 }
406 338
407 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); 339 ['withCredentials', 'bandwidth'].forEach((option) => {
408 340 if (typeof this.source_[option] !== 'undefined') {
409 // load the MediaSource into the player 341 this.options_[option] = this.source_[option];
410 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
411
412 this.options_ = {};
413 if (typeof this.source_.withCredentials !== 'undefined') {
414 this.options_.withCredentials = this.source_.withCredentials;
415 } else if (videojs.options.hls) {
416 this.options_.withCredentials = videojs.options.hls.withCredentials;
417 }
418 this.playlists = new Hls.PlaylistLoader(this.source_.src,
419 this.tech_.hls,
420 this.options_.withCredentials);
421
422 this.tech_.one('canplay', this.setupFirstPlay.bind(this));
423
424 this.playlists.on('loadedmetadata', () => {
425 oldMediaPlaylist = this.playlists.media();
426
427 // if this isn't a live video and preload permits, start
428 // downloading segments
429 if (oldMediaPlaylist.endList &&
430 this.tech_.preload() !== 'metadata' &&
431 this.tech_.preload() !== 'none') {
432 this.loadingState_ = 'segments';
433 } 342 }
434
435 this.setupSourceBuffer_();
436 this.setupFirstPlay();
437 this.fillBuffer();
438 this.tech_.trigger('loadedmetadata');
439 }); 343 });
440 344 this.options_.url = this.source_.src;
441 this.playlists.on('error', () => { 345 this.options_.tech = this.tech_;
442 this.blacklistCurrentPlaylist_(this.playlists.error); 346 this.options_.externHls = Hls;
347 this.options_.bandwidth = this.bandwidth;
348 this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
349 // `this` in selectPlaylist should be the HlsHandler for backwards
350 // compatibility with < v2
351 this.masterPlaylistController_.selectPlaylist =
352 Hls.STANDARD_PLAYLIST_SELECTOR.bind(this);
353
354 // re-expose some internal objects for backwards compatibility with < v2
355 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_;
356 this.mediaSource = this.masterPlaylistController_.mediaSource;
357
358 // Proxy assignment of some properties to the master playlist
359 // controller. Using a custom property for backwards compatibility
360 // with < v2
361 Object.defineProperties(this, {
362 selectPlaylist: {
363 get() {
364 return this.masterPlaylistController_.selectPlaylist;
365 },
366 set(selectPlaylist) {
367 this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this);
368 }
369 },
370 bandwidth: {
371 get() {
372 return this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
373 },
374 set(bandwidth) {
375 this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth;
376 }
377 }
443 }); 378 });
444 379
445 this.playlists.on('loadedplaylist', () => { 380 this.tech_.one('canplay',
446 let updatedPlaylist = this.playlists.media(); 381 this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_));
447 let seekable; 382
383 this.masterPlaylistController_.on('sourceopen', () => {
384 this.tech_.audioTracks().addEventListener('change', this.audioTrackChange_);
385 });
448 386
449 if (!updatedPlaylist) { 387 this.masterPlaylistController_.on('audioinfo', (e) => {
450 // select the initial variant 388 if (!videojs.browser.IS_FIREFOX ||
451 this.playlists.media(this.selectPlaylist()); 389 !this.audioInfo_ ||
390 !objectChanged(this.audioInfo_, e.info)) {
391 this.audioInfo_ = e.info;
452 return; 392 return;
453 } 393 }
454 394
455 this.updateDuration(this.playlists.media()); 395 let error = 'had different audio properties (channels, sample rate, etc.) ' +
396 'or changed in some other way. This behavior is currently ' +
397 'unsupported in Firefox due to an issue: \n\n' +
398 'https://bugzilla.mozilla.org/show_bug.cgi?id=1247138\n\n';
399
400 let enabledTrack;
401 let defaultTrack;
402
403 this.masterPlaylistController_.audioTracks_.forEach((t) => {
404 if (!defaultTrack && t.default) {
405 defaultTrack = t;
406 }
407
408 if (!enabledTrack && t.enabled) {
409 enabledTrack = t;
410 }
411 });
456 412
457 // update seekable 413 // they did not switch audiotracks
458 seekable = this.seekable(); 414 // blacklist the current playlist
459 if (this.duration() === Infinity && 415 if (!enabledTrack.getLoader(this.activeAudioGroup_())) {
460 seekable.length !== 0) { 416 error = `The rendition that we tried to switch to ${error}` +
461 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0)); 417 'Unfortunately that means we will have to blacklist ' +
418 'the current playlist and switch to another. Sorry!';
419 this.masterPlaylistController_.blacklistCurrentPlaylist();
420 } else {
421 error = `The audio track '${enabledTrack.label}' that we tried to ` +
422 `switch to ${error} Unfortunately this means we will have to ` +
423 `return you to the main track '${defaultTrack.label}'. Sorry!`;
424 defaultTrack.enabled = true;
425 this.tech_.audioTracks().removeTrack(enabledTrack);
462 } 426 }
463 427
464 oldMediaPlaylist = updatedPlaylist; 428 videojs.log.warn(error);
429 this.masterPlaylistController_.useAudio();
465 }); 430 });
466 431 this.masterPlaylistController_.on('selectedinitialmedia', () => {
467 this.playlists.on('mediachange', () => { 432 // clear current audioTracks
468 this.tech_.trigger({ 433 this.tech_.clearTracks('audio');
469 type: 'mediachange', 434 this.masterPlaylistController_.audioTracks_.forEach((track) => {
470 bubbles: true 435 this.tech_.audioTracks().addTrack(track);
471 }); 436 });
472 }); 437 });
473 438
439 // the bandwidth of the primary segment loader is our best
440 // estimate of overall bandwidth
441 this.on(this.masterPlaylistController_, 'progress', function() {
442 this.bandwidth = this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
443 this.tech_.trigger('progress');
444 });
445
474 // do nothing if the tech has been disposed already 446 // do nothing if the tech has been disposed already
475 // this can occur if someone sets the src in player.ready(), for instance 447 // this can occur if someone sets the src in player.ready(), for instance
476 if (!this.tech_.el()) { 448 if (!this.tech_.el()) {
477 return; 449 return;
478 } 450 }
479 451
480 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); 452 this.tech_.src(videojs.URL.createObjectURL(
481 } 453 this.masterPlaylistController_.mediaSource));
482 handleSourceOpen() {
483 // Only attempt to create the source buffer if none already exist.
484 // handleSourceOpen is also called when we are "re-opening" a source buffer
485 // after `endOfStream` has been called (in response to a seek for instance)
486 if (!this.sourceBuffer) {
487 this.setupSourceBuffer_();
488 }
489
490 // if autoplay is enabled, begin playback. This is duplicative of
491 // code in video.js but is required because play() must be invoked
492 // *after* the media source has opened.
493 // NOTE: moving this invocation of play() after
494 // sourceBuffer.appendBuffer() below caused live streams with
495 // autoplay to stall
496 if (this.tech_.autoplay()) {
497 this.play();
498 }
499 } 454 }
500 455
501 /** 456 /**
502 * Blacklist playlists that are known to be codec or 457 * a helper for grabbing the active audio group from MasterPlaylistController
503 * stream-incompatible with the SourceBuffer configuration. For
504 * instance, Media Source Extensions would cause the video element to
505 * stall waiting for video data if you switched from a variant with
506 * video and audio to an audio-only one.
507 * 458 *
508 * @param media {object} a media playlist compatible with the current 459 * @private
509 * set of SourceBuffers. Variants in the current master playlist that
510 * do not appear to have compatible codec or stream configurations
511 * will be excluded from the default playlist selection algorithm
512 * indefinitely.
513 */ 460 */
514 excludeIncompatibleVariants_(media) { 461 activeAudioGroup_() {
515 let master = this.playlists.master; 462 return this.masterPlaylistController_.activeAudioGroup();
516 let codecCount = 2;
517 let videoCodec = null;
518 let audioProfile = null;
519 let codecs;
520
521 if (media.attributes && media.attributes.CODECS) {
522 codecs = parseCodecs(media.attributes.CODECS);
523 videoCodec = codecs.videoCodec;
524 audioProfile = codecs.audioProfile;
525 codecCount = codecs.codecCount;
526 }
527 master.playlists.forEach(function(variant) {
528 let variantCodecs = {
529 codecCount: 2,
530 videoCodec: null,
531 audioProfile: null
532 };
533
534 if (variant.attributes && variant.attributes.CODECS) {
535 variantCodecs = parseCodecs(variant.attributes.CODECS);
536 }
537
538 // if the streams differ in the presence or absence of audio or
539 // video, they are incompatible
540 if (variantCodecs.codecCount !== codecCount) {
541 variant.excludeUntil = Infinity;
542 }
543
544 // if h.264 is specified on the current playlist, some flavor of
545 // it must be specified on all compatible variants
546 if (variantCodecs.videoCodec !== videoCodec) {
547 variant.excludeUntil = Infinity;
548 }
549 // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
550 // AAC audio in Chrome 46. Don't mix the two.
551 if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
552 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
553 variant.excludeUntil = Infinity;
554 }
555 });
556 } 463 }
557 464
558 setupSourceBuffer_() { 465 /**
559 let media = this.playlists.media(); 466 * Begin playing the video.
560 let mimeType; 467 */
561 468 play() {
562 // wait until a media playlist is available and the Media Source is 469 this.masterPlaylistController_.play();
563 // attached
564 if (!media || this.mediaSource.readyState !== 'open') {
565 return;
566 }
567
568 // if the codecs were explicitly specified, pass them along to the
569 // source buffer
570 mimeType = 'video/mp2t';
571 if (media.attributes && media.attributes.CODECS) {
572 mimeType += '; codecs="' + media.attributes.CODECS + '"';
573 }
574 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
575
576 // exclude any incompatible variant streams from future playlist
577 // selection
578 this.excludeIncompatibleVariants_(media);
579
580 // transition the sourcebuffer to the ended state if we've hit the end of
581 // the playlist
582 this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this));
583 } 470 }
584 471
585 /** 472 /**
586 * Seek to the latest media position if this is a live video and the 473 * a wrapper around the function in MasterPlaylistController
587 * player and video are loaded and initialized.
588 */ 474 */
589 setupFirstPlay() { 475 setCurrentTime(currentTime) {
590 let seekable; 476 this.masterPlaylistController_.setCurrentTime(currentTime);
591 let media = this.playlists.media();
592
593 // check that everything is ready to begin buffering
594
595 // 1) the video is a live stream of unknown duration
596 if (this.duration() === Infinity &&
597
598 // 2) the player has not played before and is not paused
599 this.tech_.played().length === 0 &&
600 !this.tech_.paused() &&
601
602 // 3) the Media Source and Source Buffers are ready
603 this.sourceBuffer &&
604
605 // 4) the active media playlist is available
606 media &&
607
608 // 5) the video element or flash player is in a readyState of
609 // at least HAVE_FUTURE_DATA
610 this.tech_.readyState() >= 1) {
611
612 // trigger the playlist loader to start "expired time"-tracking
613 this.playlists.trigger('firstplay');
614
615 // seek to the latest media position for live videos
616 seekable = this.seekable();
617 if (seekable.length) {
618 this.tech_.setCurrentTime(seekable.end(0));
619 }
620 }
621 } 477 }
622 478
623 /** 479 /**
624 * Begin playing the video. 480 * a wrapper around the function in MasterPlaylistController
625 */ 481 */
626 play() { 482 duration() {
627 this.loadingState_ = 'segments'; 483 return this.masterPlaylistController_.duration();
484 }
628 485
629 if (this.tech_.ended()) { 486 /**
630 this.tech_.setCurrentTime(0); 487 * a wrapper around the function in MasterPlaylistController
631 } 488 */
489 seekable() {
490 return this.masterPlaylistController_.seekable();
491 }
632 492
633 if (this.tech_.played().length === 0) { 493 /**
634 return this.setupFirstPlay(); 494 * Abort all outstanding work and cleanup.
495 */
496 dispose() {
497 if (this.masterPlaylistController_) {
498 this.masterPlaylistController_.dispose();
635 } 499 }
500 this.tech_.audioTracks().removeEventListener('change', this.audioTrackChange_);
636 501
637 // if the viewer has paused and we fell out of the live window, 502 super.dispose();
638 // seek forward to the earliest available position
639 if (this.duration() === Infinity) {
640 if (this.tech_.currentTime() < this.seekable().start(0)) {
641 this.tech_.setCurrentTime(this.seekable().start(0));
642 }
643 }
644 }
645
646 setCurrentTime(currentTime) {
647 let buffered = this.findBufferedRange_();
648
649 if (!(this.playlists && this.playlists.media())) {
650 // return immediately if the metadata is not ready yet
651 return 0;
652 }
653
654 // it's clearly an edge-case but don't thrown an error if asked to
655 // seek within an empty playlist
656 if (!this.playlists.media().segments) {
657 return 0;
658 }
659
660 // if the seek location is already buffered, continue buffering as
661 // usual
662 if (buffered && buffered.length) {
663 return currentTime;
664 }
665
666 // if we are in the middle of appending a segment, let it finish up
667 if (this.pendingSegment_ && this.pendingSegment_.buffered) {
668 return currentTime;
669 }
670
671 this.lastSegmentLoaded_ = null;
672
673 // cancel outstanding requests and buffer appends
674 this.cancelSegmentXhr();
675
676 // abort outstanding key requests, if necessary
677 if (this.keyXhr_) {
678 this.keyXhr_.aborted = true;
679 this.cancelKeyXhr();
680 }
681
682 // begin filling the buffer at the new position
683 this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
684 }
685
686 duration() {
687 let playlists = this.playlists;
688
689 if (!playlists) {
690 return 0;
691 }
692
693 if (this.mediaSource) {
694 return this.mediaSource.duration;
695 }
696
697 return Hls.Playlist.duration(playlists.media());
698 }
699
700 seekable() {
701 let media;
702 let seekable;
703
704 if (!this.playlists) {
705 return videojs.createTimeRanges();
706 }
707 media = this.playlists.media();
708 if (!media) {
709 return videojs.createTimeRanges();
710 }
711
712 seekable = Hls.Playlist.seekable(media);
713 if (seekable.length === 0) {
714 return seekable;
715 }
716
717 // if the seekable start is zero, it may be because the player has
718 // been paused for a long time and stopped buffering. in that case,
719 // fall back to the playlist loader's running estimate of expired
720 // time
721 if (seekable.start(0) === 0) {
722 return videojs.createTimeRanges([[this.playlists.expired_,
723 this.playlists.expired_ + seekable.end(0)]]);
724 }
725
726 // seekable has been calculated based on buffering video data so it
727 // can be returned directly
728 return seekable;
729 }
730
731 /**
732 * Update the player duration
733 */
734 updateDuration(playlist) {
735 let oldDuration = this.mediaSource.duration;
736 let newDuration = Hls.Playlist.duration(playlist);
737 let buffered = this.tech_.buffered();
738 let setDuration = () => {
739 this.mediaSource.duration = newDuration;
740 this.tech_.trigger('durationchange');
741
742 this.mediaSource.removeEventListener('sourceopen', setDuration);
743 };
744
745 if (buffered.length > 0) {
746 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
747 }
748
749 // if the duration has changed, invalidate the cached value
750 if (oldDuration !== newDuration) {
751 // update the duration
752 if (this.mediaSource.readyState !== 'open') {
753 this.mediaSource.addEventListener('sourceopen', setDuration);
754 } else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
755 this.mediaSource.duration = newDuration;
756 this.tech_.trigger('durationchange');
757 }
758 }
759 }
760
761 /**
762 * Clear all buffers and reset any state relevant to the current
763 * source. After this function is called, the tech should be in a
764 * state suitable for switching to a different video.
765 */
766 resetSrc_() {
767 this.cancelSegmentXhr();
768 this.cancelKeyXhr();
769
770 if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
771 this.sourceBuffer.abort();
772 }
773 }
774
775 cancelKeyXhr() {
776 if (this.keyXhr_) {
777 this.keyXhr_.onreadystatechange = null;
778 this.keyXhr_.abort();
779 this.keyXhr_ = null;
780 }
781 }
782
783 cancelSegmentXhr() {
784 if (this.segmentXhr_) {
785 // Prevent error handler from running.
786 this.segmentXhr_.onreadystatechange = null;
787 this.segmentXhr_.abort();
788 this.segmentXhr_ = null;
789 }
790
791 // clear out the segment being processed
792 this.pendingSegment_ = null;
793 }
794
795 /**
796 * Abort all outstanding work and cleanup.
797 */
798 dispose() {
799 this.stopCheckingBuffer_();
800
801 if (this.playlists) {
802 this.playlists.dispose();
803 }
804
805 this.resetSrc_();
806 super.dispose();
807 }
808
809 /**
810 * Chooses the appropriate media playlist based on the current
811 * bandwidth estimate and the player size.
812 * @return the highest bitrate playlist less than the currently detected
813 * bandwidth, accounting for some amount of bandwidth variance
814 */
815 selectPlaylist() {
816 let effectiveBitrate;
817 let sortedPlaylists = this.playlists.master.playlists.slice();
818 let bandwidthPlaylists = [];
819 let now = +new Date();
820 let i;
821 let variant;
822 let bandwidthBestVariant;
823 let resolutionPlusOne;
824 let resolutionPlusOneAttribute;
825 let resolutionBestVariant;
826 let width;
827 let height;
828
829 sortedPlaylists.sort(Hls.comparePlaylistBandwidth);
830
831 // filter out any playlists that have been excluded due to
832 // incompatible configurations or playback errors
833 sortedPlaylists = sortedPlaylists.filter((localVariant) => {
834 if (typeof localVariant.excludeUntil !== 'undefined') {
835 return now >= localVariant.excludeUntil;
836 }
837 return true;
838 });
839
840 // filter out any variant that has greater effective bitrate
841 // than the current estimated bandwidth
842 i = sortedPlaylists.length;
843 while (i--) {
844 variant = sortedPlaylists[i];
845
846 // ignore playlists without bandwidth information
847 if (!variant.attributes || !variant.attributes.BANDWIDTH) {
848 continue;
849 }
850
851 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
852
853 if (effectiveBitrate < this.bandwidth) {
854 bandwidthPlaylists.push(variant);
855
856 // since the playlists are sorted in ascending order by
857 // bandwidth, the first viable variant is the best
858 if (!bandwidthBestVariant) {
859 bandwidthBestVariant = variant;
860 }
861 }
862 }
863
864 i = bandwidthPlaylists.length;
865
866 // sort variants by resolution
867 bandwidthPlaylists.sort(Hls.comparePlaylistResolution);
868
869 // forget our old variant from above,
870 // or we might choose that in high-bandwidth scenarios
871 // (this could be the lowest bitrate rendition as we go through all of them above)
872 variant = null;
873
874 width = parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10);
875 height = parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10);
876
877 // iterate through the bandwidth-filtered playlists and find
878 // best rendition by player dimension
879 while (i--) {
880 variant = bandwidthPlaylists[i];
881
882 // ignore playlists without resolution information
883 if (!variant.attributes ||
884 !variant.attributes.RESOLUTION ||
885 !variant.attributes.RESOLUTION.width ||
886 !variant.attributes.RESOLUTION.height) {
887 continue;
888 }
889
890 // since the playlists are sorted, the first variant that has
891 // dimensions less than or equal to the player size is the best
892
893 let variantResolution = variant.attributes.RESOLUTION;
894
895 if (variantResolution.width === width &&
896 variantResolution.height === height) {
897 // if we have the exact resolution as the player use it
898 resolutionPlusOne = null;
899 resolutionBestVariant = variant;
900 break;
901 } else if (variantResolution.width < width &&
902 variantResolution.height < height) {
903 // if both dimensions are less than the player use the
904 // previous (next-largest) variant
905 break;
906 } else if (!resolutionPlusOne ||
907 (variantResolution.width < resolutionPlusOneAttribute.width &&
908 variantResolution.height < resolutionPlusOneAttribute.height)) {
909 // If we still haven't found a good match keep a
910 // reference to the previous variant for the next loop
911 // iteration
912
913 // By only saving variants if they are smaller than the
914 // previously saved variant, we ensure that we also pick
915 // the highest bandwidth variant that is just-larger-than
916 // the video player
917 resolutionPlusOne = variant;
918 resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION;
919 }
920 }
921
922 // fallback chain of variants
923 return resolutionPlusOne ||
924 resolutionBestVariant ||
925 bandwidthBestVariant ||
926 sortedPlaylists[0];
927 }
928
929 /**
930 * Periodically request new segments and append video data.
931 */
932 checkBuffer_() {
933 // calling this method directly resets any outstanding buffer checks
934 if (this.checkBufferTimeout_) {
935 window.clearTimeout(this.checkBufferTimeout_);
936 this.checkBufferTimeout_ = null;
937 }
938
939 this.fillBuffer();
940 this.drainBuffer();
941
942 // wait awhile and try again
943 this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this),
944 bufferCheckInterval);
945 }
946
947 /**
948 * Setup a periodic task to request new segments if necessary and
949 * append bytes into the SourceBuffer.
950 */
951 startCheckingBuffer_() {
952 this.checkBuffer_();
953 }
954
955 /**
956 * Stop the periodic task requesting new segments and feeding the
957 * SourceBuffer.
958 */
959 stopCheckingBuffer_() {
960 if (this.checkBufferTimeout_) {
961 window.clearTimeout(this.checkBufferTimeout_);
962 this.checkBufferTimeout_ = null;
963 }
964 }
965
966 /**
967 * Determines whether there is enough video data currently in the buffer
968 * and downloads a new segment if the buffered time is less than the goal.
969 * @param seekToTime (optional) {number} the offset into the downloaded segment
970 * to seek to, in seconds
971 */
972 fillBuffer(mediaIndex) {
973 let tech = this.tech_;
974 let currentTime = tech.currentTime();
975 let hasBufferedContent = (this.tech_.buffered().length !== 0);
976 let currentBuffered = this.findBufferedRange_();
977 let outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
978 let currentBufferedEnd = 0;
979 let bufferedTime = 0;
980 let segment;
981 let segmentInfo;
982 let segmentTimestampOffset;
983
984 // if preload is set to "none", do not download segments until playback is requested
985 if (this.loadingState_ !== 'segments') {
986 return;
987 }
988
989 // if a video has not been specified, do nothing
990 if (!tech.currentSrc() || !this.playlists) {
991 return;
992 }
993
994 // if there is a request already in flight, do nothing
995 if (this.segmentXhr_) {
996 return;
997 }
998
999 // wait until the buffer is up to date
1000 if (this.pendingSegment_) {
1001 return;
1002 }
1003
1004 // if no segments are available, do nothing
1005 if (this.playlists.state === 'HAVE_NOTHING' ||
1006 !this.playlists.media() ||
1007 !this.playlists.media().segments) {
1008 return;
1009 }
1010
1011 // if a playlist switch is in progress, wait for it to finish
1012 if (this.playlists.state === 'SWITCHING_MEDIA') {
1013 return;
1014 }
1015
1016 if (typeof mediaIndex === 'undefined') {
1017 if (currentBuffered && currentBuffered.length) {
1018 currentBufferedEnd = currentBuffered.end(0);
1019 mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd);
1020 bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
1021
1022 // if there is plenty of content in the buffer and we're not
1023 // seeking, relax for awhile
1024 if (bufferedTime >= Hls.GOAL_BUFFER_LENGTH) {
1025 return;
1026 }
1027 } else {
1028 mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
1029 }
1030 }
1031 segment = this.playlists.media().segments[mediaIndex];
1032
1033 // if the video has finished downloading
1034 if (!segment) {
1035 return;
1036 }
1037
1038 // we have entered a state where we are fetching the same segment,
1039 // try to walk forward
1040 if (this.lastSegmentLoaded_ &&
1041 this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) &&
1042 this.lastSegmentLoaded_.byterange === segment.byterange) {
1043 return this.fillBuffer(mediaIndex + 1);
1044 }
1045
1046 // package up all the work to append the segment
1047 segmentInfo = {
1048 // resolve the segment URL relative to the playlist
1049 uri: this.playlistUriToUrl(segment.uri),
1050 // the segment's mediaIndex & mediaSequence at the time it was requested
1051 mediaIndex,
1052 mediaSequence: this.playlists.media().mediaSequence,
1053 // the segment's playlist
1054 playlist: this.playlists.media(),
1055 // The state of the buffer when this segment was requested
1056 currentBufferedEnd,
1057 // unencrypted bytes of the segment
1058 bytes: null,
1059 // when a key is defined for this segment, the encrypted bytes
1060 encryptedBytes: null,
1061 // optionally, the decrypter that is unencrypting the segment
1062 decrypter: null,
1063 // the state of the buffer before a segment is appended will be
1064 // stored here so that the actual segment duration can be
1065 // determined after it has been appended
1066 buffered: null,
1067 // The target timestampOffset for this segment when we append it
1068 // to the source buffer
1069 timestampOffset: null
1070 };
1071
1072 if (mediaIndex > 0) {
1073 segmentTimestampOffset = Hls.Playlist.duration(segmentInfo.playlist,
1074 segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_;
1075 }
1076
1077 if (this.tech_.seeking() && outsideBufferedRanges) {
1078 // If there are discontinuities in the playlist, we can't be sure of anything
1079 // related to time so we reset the timestamp offset and start appending data
1080 // anew on every seek
1081 if (segmentInfo.playlist.discontinuityStarts.length) {
1082 segmentInfo.timestampOffset = segmentTimestampOffset;
1083 }
1084 } else if (segment.discontinuity && currentBuffered.length) {
1085 // If we aren't seeking and are crossing a discontinuity, we should set
1086 // timestampOffset for new segments to be appended the end of the current
1087 // buffered time-range
1088 segmentInfo.timestampOffset = currentBuffered.end(0);
1089 } else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
1090 // If we are trying to play at a position that is not zero but we aren't
1091 // currently seeking according to the video element
1092 segmentInfo.timestampOffset = segmentTimestampOffset;
1093 }
1094
1095 this.loadSegment(segmentInfo);
1096 }
1097
1098 playlistUriToUrl(segmentRelativeUrl) {
1099 let playListUrl;
1100
1101 // resolve the segment URL relative to the playlist
1102 if (this.playlists.media().uri === this.source_.src) {
1103 playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl);
1104 } else {
1105 playListUrl =
1106 resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''),
1107 segmentRelativeUrl);
1108 }
1109 return playListUrl;
1110 }
1111
1112 /*
1113 * Turns segment byterange into a string suitable for use in
1114 * HTTP Range requests
1115 */
1116 byterangeStr_(byterange) {
1117 let byterangeStart;
1118 let byterangeEnd;
1119
1120 // `byterangeEnd` is one less than `offset + length` because the HTTP range
1121 // header uses inclusive ranges
1122 byterangeEnd = byterange.offset + byterange.length - 1;
1123 byterangeStart = byterange.offset;
1124 return 'bytes=' + byterangeStart + '-' + byterangeEnd;
1125 }
1126
1127 /*
1128 * Defines headers for use in the xhr request for a particular segment.
1129 */
1130 segmentXhrHeaders_(segment) {
1131 let headers = {};
1132
1133 if ('byterange' in segment) {
1134 headers.Range = this.byterangeStr_(segment.byterange);
1135 }
1136 return headers;
1137 }
1138
1139 /*
1140 * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived.
1141 * Expects an object with:
1142 * * `roundTripTime` - the round trip time for the request we're setting the time for
1143 * * `bandwidth` - the bandwidth we want to set
1144 * * `bytesReceived` - amount of bytes downloaded
1145 * `bandwidth` is the only required property.
1146 */
1147 setBandwidth(localXhr) {
1148 // calculate the download bandwidth
1149 this.segmentXhrTime = localXhr.roundTripTime;
1150 this.bandwidth = localXhr.bandwidth;
1151 this.bytesReceived += localXhr.bytesReceived || 0;
1152
1153 this.tech_.trigger('bandwidthupdate');
1154 }
1155
1156 /*
1157 * Blacklists a playlist when an error occurs for a set amount of time
1158 * making it unavailable for selection by the rendition selection algorithm
1159 * and then forces a new playlist (rendition) selection.
1160 */
1161 blacklistCurrentPlaylist_(error) {
1162 let currentPlaylist;
1163 let nextPlaylist;
1164
1165 // If the `error` was generated by the playlist loader, it will contain
1166 // the playlist we were trying to load (but failed) and that should be
1167 // blacklisted instead of the currently selected playlist which is likely
1168 // out-of-date in this scenario
1169 currentPlaylist = error.playlist || this.playlists.media();
1170
1171 // If there is no current playlist, then an error occurred while we were
1172 // trying to load the master OR while we were disposing of the tech
1173 if (!currentPlaylist) {
1174 this.error = error;
1175 return this.mediaSource.endOfStream('network');
1176 }
1177
1178 // Blacklist this playlist
1179 currentPlaylist.excludeUntil = Date.now() + blacklistDuration;
1180
1181 // Select a new playlist
1182 nextPlaylist = this.selectPlaylist();
1183
1184 if (nextPlaylist) {
1185 videojs.log.warn('Problem encountered with the current ' +
1186 'HLS playlist. Switching to another playlist.');
1187
1188 return this.playlists.media(nextPlaylist);
1189 }
1190 videojs.log.warn('Problem encountered with the current ' +
1191 'HLS playlist. No suitable alternatives found.');
1192 // We have no more playlists we can select so we must fail
1193 this.error = error;
1194 return this.mediaSource.endOfStream('network');
1195 }
1196
1197 loadSegment(segmentInfo) {
1198 let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
1199 let removeToTime = 0;
1200 let seekable = this.seekable();
1201 let currentTime = this.tech_.currentTime();
1202
1203 // Chrome has a hard limit of 150mb of
1204 // buffer and a very conservative "garbage collector"
1205 // We manually clear out the old buffer to ensure
1206 // we don't trigger the QuotaExceeded error
1207 // on the source buffer during subsequent appends
1208 if (this.sourceBuffer && !this.sourceBuffer.updating) {
1209 // If we have a seekable range use that as the limit for what can be removed safely
1210 // otherwise remove anything older than 1 minute before the current play head
1211 if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) {
1212 removeToTime = seekable.start(0);
1213 } else {
1214 removeToTime = currentTime - 60;
1215 }
1216
1217 if (removeToTime > 0) {
1218 this.sourceBuffer.remove(0, removeToTime);
1219 }
1220 }
1221
1222 // if the segment is encrypted, request the key
1223 if (segment.key) {
1224 this.fetchKey_(segment);
1225 }
1226
1227 // request the next segment
1228 this.segmentXhr_ = this.tech_.hls.xhr({
1229 uri: segmentInfo.uri,
1230 responseType: 'arraybuffer',
1231 withCredentials: this.source_.withCredentials,
1232 // Set xhr timeout to 150% of the segment duration to allow us
1233 // some time to switch renditions in the event of a catastrophic
1234 // decrease in network performance or a server issue.
1235 timeout: (segment.duration * 1.5) * 1000,
1236 headers: this.segmentXhrHeaders_(segment)
1237 }, (error, request) => {
1238 // This is a timeout of a previously aborted segment request
1239 // so simply ignore it
1240 if (!this.segmentXhr_ || request !== this.segmentXhr_) {
1241 return;
1242 }
1243
1244 // the segment request is no longer outstanding
1245 this.segmentXhr_ = null;
1246
1247 // if a segment request times out, we may have better luck with another playlist
1248 if (request.timedout) {
1249 this.bandwidth = 1;
1250 return this.playlists.media(this.selectPlaylist());
1251 }
1252
1253 // otherwise, trigger a network error
1254 if (!request.aborted && error) {
1255 return this.blacklistCurrentPlaylist_({
1256 status: request.status,
1257 message: 'HLS segment request error at URL: ' + segmentInfo.uri,
1258 code: (request.status >= 500) ? 4 : 2
1259 });
1260 }
1261
1262 // stop processing if the request was aborted
1263 if (!request.response) {
1264 return;
1265 }
1266
1267 this.lastSegmentLoaded_ = segment;
1268 this.setBandwidth(request);
1269
1270 if (segment.key) {
1271 segmentInfo.encryptedBytes = new Uint8Array(request.response);
1272 } else {
1273 segmentInfo.bytes = new Uint8Array(request.response);
1274 }
1275
1276 this.pendingSegment_ = segmentInfo;
1277
1278 this.tech_.trigger('progress');
1279 this.drainBuffer();
1280
1281 // figure out what stream the next segment should be downloaded from
1282 // with the updated bandwidth information
1283 this.playlists.media(this.selectPlaylist());
1284 });
1285
1286 }
1287
1288 drainBuffer() {
1289 let segmentInfo;
1290 let mediaIndex;
1291 let playlist;
1292 let bytes;
1293 let segment;
1294 let decrypter;
1295 let segIv;
1296
1297 // if the buffer is empty or the source buffer hasn't been created
1298 // yet, do nothing
1299 if (!this.pendingSegment_ || !this.sourceBuffer) {
1300 return;
1301 }
1302
1303 // the pending segment has already been appended and we're waiting
1304 // for updateend to fire
1305 if (this.pendingSegment_.buffered) {
1306 return;
1307 }
1308
1309 // we can't append more data if the source buffer is busy processing
1310 // what we've already sent
1311 if (this.sourceBuffer.updating) {
1312 return;
1313 }
1314
1315 segmentInfo = this.pendingSegment_;
1316 mediaIndex = segmentInfo.mediaIndex;
1317 playlist = segmentInfo.playlist;
1318 bytes = segmentInfo.bytes;
1319 segment = playlist.segments[mediaIndex];
1320
1321 if (segment.key && !bytes) {
1322 // this is an encrypted segment
1323 // if the key download failed, we want to skip this segment
1324 // but if the key hasn't downloaded yet, we want to try again later
1325 if (keyFailed(segment.key)) {
1326 return this.blacklistCurrentPlaylist_({
1327 message: 'HLS segment key request error.',
1328 code: 4
1329 });
1330 } else if (!segment.key.bytes) {
1331 // waiting for the key bytes, try again later
1332 return;
1333 } else if (segmentInfo.decrypter) {
1334 // decryption is in progress, try again later
1335 return;
1336 }
1337 // if the media sequence is greater than 2^32, the IV will be incorrect
1338 // assuming 10s segments, that would be about 1300 years
1339 segIv = segment.key.iv ||
1340 new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
1341
1342 // create a decrypter to incrementally decrypt the segment
1343 decrypter = new Hls.Decrypter(segmentInfo.encryptedBytes,
1344 segment.key.bytes,
1345 segIv,
1346 function(error, localBytes) {
1347 if (error) {
1348 videojs.log.warn(error);
1349 }
1350 segmentInfo.bytes = localBytes;
1351 });
1352 segmentInfo.decrypter = decrypter;
1353 return;
1354 }
1355
1356 this.pendingSegment_.buffered = this.tech_.buffered();
1357
1358 if (segmentInfo.timestampOffset !== null) {
1359 this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset;
1360 }
1361
1362 // the segment is asynchronously added to the current buffered data
1363 this.sourceBuffer.appendBuffer(bytes);
1364 }
1365
1366 updateEndHandler_() {
1367 let segmentInfo = this.pendingSegment_;
1368 let playlist;
1369 let currentMediaIndex;
1370 let currentBuffered;
1371 let seekable;
1372 let timelineUpdate;
1373 let isEndOfStream;
1374
1375 // stop here if the update errored or was aborted
1376 if (!segmentInfo) {
1377 this.pendingSegment_ = null;
1378 return;
1379 }
1380
1381 // In Firefox, the updateend event is triggered for both removing from the buffer and
1382 // adding to the buffer. To prevent this code from executing on removals, we wait for
1383 // segmentInfo to have a filled in buffered value before we continue processing.
1384 if (!segmentInfo.buffered) {
1385 return;
1386 }
1387
1388 this.pendingSegment_ = null;
1389
1390 playlist = segmentInfo.playlist;
1391 currentMediaIndex = segmentInfo.mediaIndex +
1392 (segmentInfo.mediaSequence - playlist.mediaSequence);
1393 currentBuffered = this.findBufferedRange_();
1394 isEndOfStream = detectEndOfStream(playlist, this.mediaSource, currentMediaIndex, currentBuffered);
1395
1396 // if we switched renditions don't try to add segment timeline
1397 // information to the playlist
1398 if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
1399 if (isEndOfStream) {
1400 return this.mediaSource.endOfStream();
1401 }
1402 return this.fillBuffer();
1403 }
1404
1405 // when seeking to the beginning of the seekable range, it's
1406 // possible that imprecise timing information may cause the seek to
1407 // end up earlier than the start of the range
1408 // in that case, seek again
1409 seekable = this.seekable();
1410 if (this.tech_.seeking() &&
1411 currentBuffered.length === 0) {
1412 if (seekable.length &&
1413 this.tech_.currentTime() < seekable.start(0)) {
1414 let next = this.findNextBufferedRange_();
1415
1416 if (next.length) {
1417 videojs.log('tried seeking to', this.tech_.currentTime(),
1418 'but that was too early, retrying at', next.start(0));
1419 this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR);
1420 }
1421 }
1422 }
1423
1424 timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
1425 this.tech_.buffered());
1426
1427 // Update segment meta-data (duration and end-point) based on timeline
1428 updateSegmentMetadata(playlist, currentMediaIndex, timelineUpdate);
1429
1430 // If we decide to signal the end of stream, then we can return instead
1431 // of trying to fetch more segments
1432 if (isEndOfStream) {
1433 return this.mediaSource.endOfStream();
1434 }
1435
1436 if (timelineUpdate !== null ||
1437 segmentInfo.buffered.length !== this.tech_.buffered().length) {
1438 this.updateDuration(playlist);
1439 // check if it's time to download the next segment
1440 this.fillBuffer();
1441 return;
1442 }
1443
1444 // the last segment append must have been entirely in the
1445 // already buffered time ranges. just buffer forward until we
1446 // find a segment that adds to the buffered time ranges and
1447 // improves subsequent media index calculations.
1448 this.fillBuffer(currentMediaIndex + 1);
1449 return;
1450 }
1451
1452 /**
1453 * Attempt to retrieve the key for a particular media segment.
1454 */
1455 fetchKey_(segment) {
1456 let key;
1457 let settings;
1458 let receiveKey;
1459
1460 // if there is a pending XHR or no segments, don't do anything
1461 if (this.keyXhr_) {
1462 return;
1463 }
1464
1465 settings = this.options_;
1466
1467 /**
1468 * Handle a key XHR response.
1469 */
1470 receiveKey = (keyRecieved) => {
1471 return (error, request) => {
1472 let view;
1473
1474 this.keyXhr_ = null;
1475
1476 if (error || !request.response || request.response.byteLength !== 16) {
1477 keyRecieved.retries = keyRecieved.retries || 0;
1478 keyRecieved.retries++;
1479 if (!request.aborted) {
1480 // try fetching again
1481 this.fetchKey_(segment);
1482 }
1483 return;
1484 }
1485
1486 view = new DataView(request.response);
1487 keyRecieved.bytes = new Uint32Array([
1488 view.getUint32(0),
1489 view.getUint32(4),
1490 view.getUint32(8),
1491 view.getUint32(12)
1492 ]);
1493
1494 // check to see if this allows us to make progress buffering now
1495 this.checkBuffer_();
1496 };
1497 };
1498
1499 key = segment.key;
1500
1501 // nothing to do if this segment is unencrypted
1502 if (!key) {
1503 return;
1504 }
1505
1506 // request the key if the retry limit hasn't been reached
1507 if (!key.bytes && !keyFailed(key)) {
1508 this.keyXhr_ = this.tech_.hls.xhr({
1509 uri: this.playlistUriToUrl(key.uri),
1510 responseType: 'arraybuffer',
1511 withCredentials: settings.withCredentials
1512 }, receiveKey(key));
1513 return;
1514 }
1515 } 503 }
1516 } 504 }
1517 505
1518 /** 506 /**
1519 * Attempts to find the buffered TimeRange that contains the specified
1520 * time, or where playback is currently happening if no specific time
1521 * is specified.
1522 * @param time (optional) {number} the time to filter on. Defaults to
1523 * currentTime.
1524 * @return a new TimeRanges object.
1525 */
1526 HlsHandler.prototype.findBufferedRange_ =
1527 filterBufferedRanges(function(start, end, time) {
1528 return start - TIME_FUDGE_FACTOR <= time &&
1529 end + TIME_FUDGE_FACTOR >= time;
1530 });
1531 /**
1532 * Returns the TimeRanges that begin at or later than the specified
1533 * time.
1534 * @param time (optional) {number} the time to filter on. Defaults to
1535 * currentTime.
1536 * @return a new TimeRanges object.
1537 */
1538 HlsHandler.prototype.findNextBufferedRange_ =
1539 filterBufferedRanges(function(start, end, time) {
1540 return start - TIME_FUDGE_FACTOR >= time;
1541 });
1542
1543 /**
1544 * The Source Handler object, which informs video.js what additional 507 * The Source Handler object, which informs video.js what additional
1545 * MIME types are supported and sets up playback. It is registered 508 * MIME types are supported and sets up playback. It is registered
1546 * automatically to the appropriate tech based on the capabilities of 509 * automatically to the appropriate tech based on the capabilities of
...@@ -1550,9 +513,16 @@ HlsHandler.prototype.findNextBufferedRange_ = ...@@ -1550,9 +513,16 @@ HlsHandler.prototype.findNextBufferedRange_ =
1550 const HlsSourceHandler = function(mode) { 513 const HlsSourceHandler = function(mode) {
1551 return { 514 return {
1552 canHandleSource(srcObj) { 515 canHandleSource(srcObj) {
516 // this forces video.js to skip this tech/mode if its not the one we have been
517 // overriden to use, by returing that we cannot handle the source.
518 if (videojs.options.hls &&
519 videojs.options.hls.mode &&
520 videojs.options.hls.mode !== mode) {
521 return false;
522 }
1553 return HlsSourceHandler.canPlayType(srcObj.type); 523 return HlsSourceHandler.canPlayType(srcObj.type);
1554 }, 524 },
1555 handleSource(source, tech) { 525 handleSource(source, tech, options) {
1556 if (mode === 'flash') { 526 if (mode === 'flash') {
1557 // We need to trigger this asynchronously to give others the chance 527 // We need to trigger this asynchronously to give others the chance
1558 // to bind to the event when a source is set at player creation 528 // to bind to the event when a source is set at player creation
...@@ -1560,10 +530,10 @@ const HlsSourceHandler = function(mode) { ...@@ -1560,10 +530,10 @@ const HlsSourceHandler = function(mode) {
1560 tech.trigger('loadstart'); 530 tech.trigger('loadstart');
1561 }, 1); 531 }, 1);
1562 } 532 }
1563 tech.hls = new HlsHandler(tech, { 533
1564 source, 534 let settings = videojs.mergeOptions(options, {hls: {mode}});
1565 mode 535
1566 }); 536 tech.hls = new HlsHandler(source, tech, settings);
1567 537
1568 tech.hls.xhr = xhrFactory(); 538 tech.hls.xhr = xhrFactory();
1569 // Use a global `before` function if specified on videojs.Hls.xhr 539 // Use a global `before` function if specified on videojs.Hls.xhr
...@@ -1576,13 +546,81 @@ const HlsSourceHandler = function(mode) { ...@@ -1576,13 +546,81 @@ const HlsSourceHandler = function(mode) {
1576 return tech.hls; 546 return tech.hls;
1577 }, 547 },
1578 canPlayType(type) { 548 canPlayType(type) {
1579 return HlsSourceHandler.canPlayType(type); 549 if (HlsSourceHandler.canPlayType(type)) {
550 return 'maybe';
551 }
552 return '';
1580 } 553 }
1581 }; 554 };
1582 }; 555 };
1583 556
557 /**
558 * A comparator function to sort two playlist object by bandwidth.
559 *
560 * @param {Object} left a media playlist object
561 * @param {Object} right a media playlist object
562 * @return {Number} Greater than zero if the bandwidth attribute of
563 * left is greater than the corresponding attribute of right. Less
564 * than zero if the bandwidth of right is greater than left and
565 * exactly zero if the two are equal.
566 */
567 Hls.comparePlaylistBandwidth = function(left, right) {
568 let leftBandwidth;
569 let rightBandwidth;
570
571 if (left.attributes && left.attributes.BANDWIDTH) {
572 leftBandwidth = left.attributes.BANDWIDTH;
573 }
574 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
575 if (right.attributes && right.attributes.BANDWIDTH) {
576 rightBandwidth = right.attributes.BANDWIDTH;
577 }
578 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
579
580 return leftBandwidth - rightBandwidth;
581 };
582
583 /**
584 * A comparator function to sort two playlist object by resolution (width).
585 * @param {Object} left a media playlist object
586 * @param {Object} right a media playlist object
587 * @return {Number} Greater than zero if the resolution.width attribute of
588 * left is greater than the corresponding attribute of right. Less
589 * than zero if the resolution.width of right is greater than left and
590 * exactly zero if the two are equal.
591 */
592 Hls.comparePlaylistResolution = function(left, right) {
593 let leftWidth;
594 let rightWidth;
595
596 if (left.attributes &&
597 left.attributes.RESOLUTION &&
598 left.attributes.RESOLUTION.width) {
599 leftWidth = left.attributes.RESOLUTION.width;
600 }
601
602 leftWidth = leftWidth || window.Number.MAX_VALUE;
603
604 if (right.attributes &&
605 right.attributes.RESOLUTION &&
606 right.attributes.RESOLUTION.width) {
607 rightWidth = right.attributes.RESOLUTION.width;
608 }
609
610 rightWidth = rightWidth || window.Number.MAX_VALUE;
611
612 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
613 // have the same media dimensions/ resolution
614 if (leftWidth === rightWidth &&
615 left.attributes.BANDWIDTH &&
616 right.attributes.BANDWIDTH) {
617 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
618 }
619 return leftWidth - rightWidth;
620 };
621
1584 HlsSourceHandler.canPlayType = function(type) { 622 HlsSourceHandler.canPlayType = function(type) {
1585 let mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; 623 let mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
1586 624
1587 // favor native HLS support if it's available 625 // favor native HLS support if it's available
1588 if (Hls.supportsNativeHls) { 626 if (Hls.supportsNativeHls) {
...@@ -1609,6 +647,8 @@ videojs.HlsHandler = HlsHandler; ...@@ -1609,6 +647,8 @@ videojs.HlsHandler = HlsHandler;
1609 videojs.HlsSourceHandler = HlsSourceHandler; 647 videojs.HlsSourceHandler = HlsSourceHandler;
1610 videojs.Hls = Hls; 648 videojs.Hls = Hls;
1611 videojs.m3u8 = m3u8; 649 videojs.m3u8 = m3u8;
650 videojs.registerComponent('Hls', Hls);
651 videojs.options.hls = videojs.options.hls || {};
1612 652
1613 module.exports = { 653 module.exports = {
1614 Hls, 654 Hls,
......
1 /** 1 /**
2 * @file xhr.js
3 */
4
5 /**
2 * A wrapper for videojs.xhr that tracks bandwidth. 6 * A wrapper for videojs.xhr that tracks bandwidth.
7 *
8 * @param {Object} options options for the XHR
9 * @param {Function} callback the callback to call when done
10 * @return {Request} the xhr request that is going to be made
3 */ 11 */
4 import {xhr as videojsXHR, mergeOptions} from 'video.js'; 12 import {xhr as videojsXHR, mergeOptions} from 'video.js';
5 13
......
1 import HlsAudioTrack from '../src/hls-audio-track';
2 import QUnit from 'qunit';
3
4 // Most of these tests will be done in video.js.AudioTrack unit tests
5 QUnit.module('HlsAudioTrack - Props');
6
7 QUnit.test('verify that props are readonly and can be set', function() {
8 let props = {
9 default: true,
10 language: 'en',
11 label: 'English',
12 autoselect: true,
13 withCredentials: true,
14 // below props won't be used, its used for checking
15 kind: 'main'
16 };
17
18 let track = new HlsAudioTrack(props);
19
20 for (let k in props) {
21 QUnit.equal(track[k], props[k], `${k} should be stored in track`);
22 }
23 });
24
25 QUnit.test('can start with a mediaGroup that has a uri', function() {
26 let props = {
27 default: true,
28 language: 'en',
29 label: 'English',
30 autoselect: true,
31 mediaGroup: 'foo',
32 withCredentials: true,
33 resolvedUri: 'http://some.test.url/playlist.m3u8',
34 // below props won't be used, its used for checking
35 enabled: true,
36 kind: 'main'
37 };
38 let track = new HlsAudioTrack(props);
39
40 QUnit.equal(track.mediaGroups_.length, 1, 'loader was created');
41 let loader = track.getLoader('foo');
42
43 QUnit.ok(loader, 'can getLoader on foo');
44
45 track.dispose();
46 QUnit.equal(track.mediaGroups_.length, 0, 'loader disposed');
47 });
48
49 QUnit.test('can start with a mediaGroup that has no uri', function() {
50 let props = {
51 default: true,
52 language: 'en',
53 label: 'English',
54 autoselect: true,
55 mediaGroup: 'foo',
56 withCredentials: true,
57 // below props won't be used, its used for checking
58 enabled: true,
59 kind: 'main'
60 };
61 let track = new HlsAudioTrack(props);
62
63 QUnit.equal(track.mediaGroups_.length, 1, 'mediaGroupLoader was created for foo');
64 QUnit.ok(!track.getLoader('foo'), 'can getLoader on foo, but it is undefined');
65
66 track.dispose();
67 QUnit.equal(track.mediaGroups_.length, 0, 'loaders disposed');
68 });
69
70 QUnit.module('HlsAudioTrack - Loader', {
71 beforeEach() {
72 this.track = new HlsAudioTrack({
73 mediaGroup: 'default',
74 default: true,
75 language: 'en',
76 label: 'English',
77 autoselect: true,
78 withCredentials: true
79 });
80 },
81 afterEach() {
82 this.track.dispose();
83 QUnit.equal(this.track.mediaGroups_.length, 0, 'zero loaders after dispose');
84 }
85 });
86
87 QUnit.test('can add a playlist loader', function() {
88 QUnit.equal(this.track.mediaGroups_.length, 1, '1 loader to start');
89
90 this.track.addLoader('foo', 'someurl');
91 this.track.addLoader('bar', 'someurl');
92 this.track.addLoader('baz', 'someurl');
93
94 QUnit.equal(this.track.mediaGroups_.length, 4, 'now has four loaders');
95 });
96
97 QUnit.test('can remove playlist loader', function() {
98 QUnit.equal(this.track.mediaGroups_.length, 1, 'one loaders to start');
99
100 this.track.addLoader('foo', 'someurl');
101 this.track.addLoader('baz', 'someurl');
102
103 QUnit.equal(this.track.mediaGroups_.length, 3, 'now has three loaders');
104
105 this.track.removeLoader('baz');
106 QUnit.equal(this.track.mediaGroups_.length, 2, 'now has two loaders');
107
108 });
...@@ -10,13 +10,10 @@ var DEFAULTS = { ...@@ -10,13 +10,10 @@ var DEFAULTS = {
10 'node_modules/sinon/pkg/sinon-ie.js', 10 'node_modules/sinon/pkg/sinon-ie.js',
11 'node_modules/video.js/dist/video.js', 11 'node_modules/video.js/dist/video.js',
12 'node_modules/video.js/dist/video-js.css', 12 'node_modules/video.js/dist/video-js.css',
13
14 'test/**/*.test.js' 13 'test/**/*.test.js'
15 ], 14 ],
16 15
17 exclude: [ 16 exclude: [],
18 'test/data/**'
19 ],
20 17
21 plugins: [ 18 plugins: [
22 'karma-browserify', 19 'karma-browserify',
...@@ -43,6 +40,13 @@ var DEFAULTS = { ...@@ -43,6 +40,13 @@ var DEFAULTS = {
43 noParse: [ 40 noParse: [
44 'test/data/**', 41 'test/data/**',
45 ] 42 ]
43 },
44
45 customLaunchers: {
46 travisChrome: {
47 base: 'Chrome',
48 flags: ['--no-sandbox']
49 }
46 } 50 }
47 }; 51 };
48 52
......
...@@ -4,12 +4,10 @@ var common = require('./common'); ...@@ -4,12 +4,10 @@ var common = require('./common');
4 4
5 module.exports = function(config) { 5 module.exports = function(config) {
6 6
7 // Travis CI should run in its available Firefox headless browser.
8 if (process.env.TRAVIS) { 7 if (process.env.TRAVIS) {
9
10 config.set(common({ 8 config.set(common({
11 browsers: ['Firefox'], 9 browsers: ['travisChrome'],
12 plugins: ['karma-firefox-launcher'] 10 plugins: ['karma-chrome-launcher']
13 })) 11 }))
14 } else { 12 } else {
15 config.set(common({ 13 config.set(common({
......
1 import QUnit from 'qunit';
2 import videojs from 'video.js';
3 import {
4 useFakeEnvironment,
5 useFakeMediaSource,
6 createPlayer,
7 standardXHRResponse,
8 openMediaSource
9 } from './test-helpers.js';
10 import MasterPlaylistController from '../src/master-playlist-controller';
11 /* eslint-disable no-unused-vars */
12 // we need this so that it can register hls with videojs
13 import { Hls } from '../src/videojs-contrib-hls';
14 /* eslint-enable no-unused-vars */
15 import Playlist from '../src/playlist';
16
17 QUnit.module('MasterPlaylistController', {
18 beforeEach() {
19 this.env = useFakeEnvironment();
20 this.clock = this.env.clock;
21 this.requests = this.env.requests;
22 this.mse = useFakeMediaSource();
23
24 // force the HLS tech to run
25 this.origSupportsNativeHls = videojs.Hls.supportsNativeHls;
26 videojs.Hls.supportsNativeHls = false;
27
28 this.player = createPlayer();
29 this.player.src({
30 src: 'manifest/master.m3u8',
31 type: 'application/vnd.apple.mpegurl'
32 });
33 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
34 },
35 afterEach() {
36 this.env.restore();
37 this.mse.restore();
38 videojs.Hls.supportsNativeHls = this.origSupportsNativeHls;
39 this.player.dispose();
40 }
41 });
42
43 QUnit.test('throws error when given an empty URL', function() {
44 let options = {
45 url: 'test',
46 currentTimeFunc: () => {},
47 tech: this.player.tech_
48 };
49
50 QUnit.ok(new MasterPlaylistController(options), 'can create with options');
51
52 options.url = '';
53 QUnit.throws(() => {
54 new MasterPlaylistController(options); // eslint-disable-line no-new
55 }, /A non-empty playlist URL is required/, 'requires a non empty url');
56 });
57
58 QUnit.test('obeys none preload option', function() {
59 this.player.preload('none');
60 // master
61 standardXHRResponse(this.requests.shift());
62 // playlist
63 standardXHRResponse(this.requests.shift());
64
65 openMediaSource(this.player, this.clock);
66
67 QUnit.equal(this.requests.length, 0, 'no segment requests');
68 });
69
70 QUnit.test('obeys auto preload option', function() {
71 this.player.preload('auto');
72 // master
73 standardXHRResponse(this.requests.shift());
74 // playlist
75 standardXHRResponse(this.requests.shift());
76
77 openMediaSource(this.player, this.clock);
78
79 QUnit.equal(this.requests.length, 1, '1 segment request');
80 });
81
82 QUnit.test('obeys metadata preload option', function() {
83 this.player.preload('metadata');
84 // master
85 standardXHRResponse(this.requests.shift());
86 // playlist
87 standardXHRResponse(this.requests.shift());
88
89 openMediaSource(this.player, this.clock);
90
91 QUnit.equal(this.requests.length, 1, '1 segment request');
92 });
93
94 QUnit.test('clears some of the buffer for a fast quality change', function() {
95 let removes = [];
96
97 // master
98 standardXHRResponse(this.requests.shift());
99 // media
100 standardXHRResponse(this.requests.shift());
101 this.masterPlaylistController.mediaSource.trigger('sourceopen');
102
103 let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
104
105 segmentLoader.sourceUpdater_.remove = function(start, end) {
106 removes.push({ start, end });
107 };
108 this.masterPlaylistController.selectPlaylist = () => {
109 return this.masterPlaylistController.master().playlists[0];
110 };
111 this.masterPlaylistController.currentTimeFunc = () => 7;
112
113 this.masterPlaylistController.fastQualityChange_();
114
115 QUnit.equal(removes.length, 1, 'removed buffered content');
116 QUnit.equal(removes[0].start, 7 + 5, 'removed from a bit after current time');
117 QUnit.equal(removes[0].end, Infinity, 'removed to the end');
118 });
119
120 QUnit.test('does not clear the buffer when no fast quality change occurs', function() {
121 let removes = [];
122
123 // master
124 standardXHRResponse(this.requests.shift());
125 // media
126 standardXHRResponse(this.requests.shift());
127 this.masterPlaylistController.mediaSource.trigger('sourceopen');
128
129 let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
130
131 segmentLoader.sourceUpdater_.remove = function(start, end) {
132 removes.push({ start, end });
133 };
134
135 this.masterPlaylistController.fastQualityChange_();
136
137 QUnit.equal(removes.length, 0, 'did not remove content');
138 });
139
140 QUnit.test('if buffered, will request second segment byte range', function() {
141 this.requests.length = 0;
142 this.player.src({
143 src: 'manifest/playlist.m3u8',
144 type: 'application/vnd.apple.mpegurl'
145 });
146 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
147
148 // mock that the user has played the video before
149 this.player.tech_.triggerReady();
150 this.clock.tick(1);
151 this.player.tech_.trigger('play');
152 this.player.tech_.played = () => videojs.createTimeRanges([[0, 20]]);
153
154 openMediaSource(this.player, this.clock);
155 // playlist
156 standardXHRResponse(this.requests[0]);
157
158 this.masterPlaylistController.mainSegmentLoader_.sourceUpdater_.buffered = () => {
159 return videojs.createTimeRanges([[0, 20]]);
160 };
161
162 // segment
163 standardXHRResponse(this.requests[1]);
164 this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
165 this.clock.tick(10 * 1000);
166 QUnit.equal(this.requests[2].headers.Range, 'bytes=1823412-2299991');
167 });
168
169 QUnit.test('re-initializes the combined playlist loader when switching sources',
170 function() {
171 openMediaSource(this.player, this.clock);
172 // master
173 standardXHRResponse(this.requests.shift());
174 // playlist
175 standardXHRResponse(this.requests.shift());
176 // segment
177 standardXHRResponse(this.requests.shift());
178 // change the source
179 this.player.src({
180 src: 'manifest/master.m3u8',
181 type: 'application/vnd.apple.mpegurl'
182 });
183 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
184 // maybe not needed if https://github.com/videojs/video.js/issues/2326 gets fixed
185 this.clock.tick(1);
186 QUnit.ok(!this.masterPlaylistController.masterPlaylistLoader_.media(),
187 'no media playlist');
188 QUnit.equal(this.masterPlaylistController.masterPlaylistLoader_.state,
189 'HAVE_NOTHING',
190 'reset the playlist loader state');
191 QUnit.equal(this.requests.length, 1, 'requested the new src');
192
193 // buffer check
194 this.clock.tick(10 * 1000);
195 QUnit.equal(this.requests.length, 1, 'did not request a stale segment');
196
197 // sourceopen
198 openMediaSource(this.player, this.clock);
199
200 QUnit.equal(this.requests.length, 1, 'made one request');
201 QUnit.ok(
202 this.requests[0].url.indexOf('master.m3u8') >= 0,
203 'requested only the new playlist'
204 );
205 });
206
207 QUnit.test('updates the combined segment loader on live playlist refreshes', function() {
208 let updates = [];
209
210 openMediaSource(this.player, this.clock);
211 // master
212 standardXHRResponse(this.requests.shift());
213 // media
214 standardXHRResponse(this.requests.shift());
215
216 this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
217 updates.push(update);
218 };
219
220 this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
221 QUnit.equal(updates.length, 1, 'updated the segment list');
222 });
223
224 QUnit.test(
225 'fires a progress event after downloading a segment from combined segment loader',
226 function() {
227 let progressCount = 0;
228
229 openMediaSource(this.player, this.clock);
230
231 // master
232 standardXHRResponse(this.requests.shift());
233 // media
234 standardXHRResponse(this.requests.shift());
235
236 this.player.tech_.on('progress', function() {
237 progressCount++;
238 });
239
240 // segment
241 standardXHRResponse(this.requests.shift());
242 this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
243 QUnit.equal(progressCount, 1, 'fired a progress event');
244 });
245
246 QUnit.test('blacklists switching from video+audio playlists to audio only', function() {
247 let audioPlaylist;
248
249 openMediaSource(this.player, this.clock);
250
251 this.player.tech_.hls.bandwidth = 1e10;
252
253 // master
254 this.requests.shift().respond(200, null,
255 '#EXTM3U\n' +
256 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
257 'media.m3u8\n' +
258 '#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
259 'media1.m3u8\n');
260 // media1
261 standardXHRResponse(this.requests.shift());
262
263 QUnit.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
264 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1],
265 'selected video+audio');
266 audioPlaylist = this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0];
267 QUnit.equal(audioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
268 });
269
270 QUnit.test('blacklists switching from audio-only playlists to video+audio', function() {
271 let videoAudioPlaylist;
272
273 openMediaSource(this.player, this.clock);
274
275 this.player.tech_.hls.bandwidth = 1;
276 // master
277 this.requests.shift().respond(200, null,
278 '#EXTM3U\n' +
279 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
280 'media.m3u8\n' +
281 '#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
282 'media1.m3u8\n');
283
284 // media1
285 standardXHRResponse(this.requests.shift());
286 QUnit.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
287 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
288 'selected audio only');
289 videoAudioPlaylist =
290 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
291 QUnit.equal(videoAudioPlaylist.excludeUntil,
292 Infinity,
293 'excluded incompatible playlist');
294 });
295
296 QUnit.test('blacklists switching from video-only playlists to video+audio', function() {
297 let videoAudioPlaylist;
298
299 openMediaSource(this.player, this.clock);
300
301 this.player.tech_.hls.bandwidth = 1;
302 // master
303 this.requests.shift()
304 .respond(200, null,
305 '#EXTM3U\n' +
306 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d"\n' +
307 'media.m3u8\n' +
308 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
309 'media1.m3u8\n');
310
311 // media
312 standardXHRResponse(this.requests.shift());
313 QUnit.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
314 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
315 'selected video only');
316 videoAudioPlaylist =
317 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
318 QUnit.equal(videoAudioPlaylist.excludeUntil,
319 Infinity,
320 'excluded incompatible playlist');
321 });
322
323 QUnit.test('blacklists switching between playlists with incompatible audio codecs',
324 function() {
325 let alternatePlaylist;
326
327 openMediaSource(this.player, this.clock);
328
329 this.player.tech_.hls.bandwidth = 1;
330 // master
331 this.requests.shift()
332 .respond(200, null,
333 '#EXTM3U\n' +
334 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
335 'media.m3u8\n' +
336 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
337 'media1.m3u8\n');
338
339 // media
340 standardXHRResponse(this.requests.shift());
341 QUnit.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
342 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
343 'selected HE-AAC stream');
344 alternatePlaylist =
345 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
346 QUnit.equal(alternatePlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
347 });
348
349 QUnit.test('updates the combined segment loader on media changes', function() {
350 let updates = [];
351
352 this.masterPlaylistController.mediaSource.trigger('sourceopen');
353
354 this.masterPlaylistController.mainSegmentLoader_.bandwidth = 1;
355
356 // master
357 standardXHRResponse(this.requests.shift());
358 // media
359 standardXHRResponse(this.requests.shift());
360
361 this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
362 updates.push(update);
363 };
364
365 // downloading the new segment will update bandwidth and cause a
366 // playlist change
367 // segment 0
368 standardXHRResponse(this.requests.shift());
369 this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
370 // media
371 standardXHRResponse(this.requests.shift());
372 QUnit.equal(updates.length, 1, 'updated the segment list');
373 });
374
375 QUnit.test('selects a playlist after main/combined segment downloads', function() {
376 let calls = 0;
377
378 this.masterPlaylistController.selectPlaylist = () => {
379 calls++;
380 return this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0];
381 };
382 this.masterPlaylistController.mediaSource.trigger('sourceopen');
383
384 // master
385 standardXHRResponse(this.requests.shift());
386 // media
387 standardXHRResponse(this.requests.shift());
388
389 // "downloaded" a segment
390 this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
391 QUnit.strictEqual(calls, 2, 'selects after the initial segment');
392
393 // and another
394 this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
395 QUnit.strictEqual(calls, 3, 'selects after additional segments');
396 });
397
398 QUnit.test('updates the duration after switching playlists', function() {
399 let selectedPlaylist = false;
400
401 this.masterPlaylistController.mediaSource.trigger('sourceopen');
402
403 this.masterPlaylistController.bandwidth = 1e20;
404
405 // master
406 standardXHRResponse(this.requests[0]);
407 // media
408 standardXHRResponse(this.requests[1]);
409
410 this.masterPlaylistController.selectPlaylist = () => {
411 selectedPlaylist = true;
412
413 // this duration should be overwritten by the playlist change
414 this.masterPlaylistController.mediaSource.duration = 0;
415 this.masterPlaylistController.mediaSource.readyState = 'open';
416
417 return this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
418 };
419
420 // segment 0
421 standardXHRResponse(this.requests[2]);
422 this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
423 // media1
424 standardXHRResponse(this.requests[3]);
425 QUnit.ok(selectedPlaylist, 'selected playlist');
426 QUnit.ok(this.masterPlaylistController.mediaSource.duration !== 0,
427 'updates the duration');
428 });
429
430 QUnit.test('seekable uses the intersection of alternate audio and combined tracks',
431 function() {
432 let origSeekable = Playlist.seekable;
433 let mainMedia = {};
434 let audioMedia = {};
435 let mainTimeRanges = [];
436 let audioTimeRanges = [];
437 let assertTimeRangesEqual = (left, right, message) => {
438 if (left.length === 0 && right.length === 0) {
439 return;
440 }
441
442 QUnit.equal(left.length, 1, message);
443 QUnit.equal(right.length, 1, message);
444
445 QUnit.equal(left.start(0), right.start(0), message);
446 QUnit.equal(left.end(0), right.end(0), message);
447 };
448
449 this.masterPlaylistController.masterPlaylistLoader_.media = () => mainMedia;
450
451 Playlist.seekable = (media) => {
452 if (media === mainMedia) {
453 return videojs.createTimeRanges(mainTimeRanges);
454 }
455 return videojs.createTimeRanges(audioTimeRanges);
456 };
457
458 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
459 videojs.createTimeRanges(),
460 'empty when main empty');
461 mainTimeRanges = [[0, 10]];
462 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
463 videojs.createTimeRanges([[0, 10]]),
464 'main when no audio');
465
466 this.masterPlaylistController.audioPlaylistLoader_ = {
467 media: () => audioMedia,
468 expired_: 0
469 };
470
471 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
472 videojs.createTimeRanges(),
473 'empty when both empty');
474 mainTimeRanges = [[0, 10]];
475 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
476 videojs.createTimeRanges(),
477 'empty when audio empty');
478 mainTimeRanges = [];
479 audioTimeRanges = [[0, 10]];
480 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
481 videojs.createTimeRanges(),
482 'empty when main empty');
483 mainTimeRanges = [[0, 10]];
484 audioTimeRanges = [[0, 10]];
485 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
486 videojs.createTimeRanges([[0, 10]]),
487 'ranges equal');
488 mainTimeRanges = [[5, 10]];
489 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
490 videojs.createTimeRanges([[5, 10]]),
491 'main later start');
492 mainTimeRanges = [[0, 10]];
493 audioTimeRanges = [[5, 10]];
494 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
495 videojs.createTimeRanges([[5, 10]]),
496 'audio later start');
497 mainTimeRanges = [[0, 9]];
498 audioTimeRanges = [[0, 10]];
499 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
500 videojs.createTimeRanges([[0, 9]]),
501 'main earlier end');
502 mainTimeRanges = [[0, 10]];
503 audioTimeRanges = [[0, 9]];
504 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
505 videojs.createTimeRanges([[0, 9]]),
506 'audio earlier end');
507 mainTimeRanges = [[1, 10]];
508 audioTimeRanges = [[0, 9]];
509 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
510 videojs.createTimeRanges([[1, 9]]),
511 'main later start, audio earlier end');
512 mainTimeRanges = [[0, 9]];
513 audioTimeRanges = [[1, 10]];
514 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
515 videojs.createTimeRanges([[1, 9]]),
516 'audio later start, main earlier end');
517 mainTimeRanges = [[2, 9]];
518 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
519 videojs.createTimeRanges([[2, 9]]),
520 'main later start, main earlier end');
521 mainTimeRanges = [[1, 10]];
522 audioTimeRanges = [[2, 9]];
523 assertTimeRangesEqual(this.masterPlaylistController.seekable(),
524 videojs.createTimeRanges([[2, 9]]),
525 'audio later start, audio earlier end');
526
527 Playlist.seekable = origSeekable;
528 });
1 import sinon from 'sinon';
2 import QUnit from 'qunit'; 1 import QUnit from 'qunit';
3 import PlaylistLoader from '../src/playlist-loader'; 2 import PlaylistLoader from '../src/playlist-loader';
4 import videojs from 'video.js';
5 import xhrFactory from '../src/xhr'; 3 import xhrFactory from '../src/xhr';
4 import { useFakeEnvironment } from './test-helpers';
5
6 // Attempts to produce an absolute URL to a given relative path 6 // Attempts to produce an absolute URL to a given relative path
7 // based on window.location.href 7 // based on window.location.href
8 const urlTo = function(path) { 8 const urlTo = function(path) {
...@@ -15,28 +15,15 @@ const urlTo = function(path) { ...@@ -15,28 +15,15 @@ const urlTo = function(path) {
15 15
16 QUnit.module('Playlist Loader', { 16 QUnit.module('Playlist Loader', {
17 beforeEach() { 17 beforeEach() {
18 // fake XHRs 18 this.env = useFakeEnvironment();
19 this.oldXHR = videojs.xhr.XMLHttpRequest; 19 this.clock = this.env.clock;
20 this.sinonXhr = sinon.useFakeXMLHttpRequest(); 20 this.requests = this.env.requests;
21 this.requests = [];
22 this.sinonXhr.onCreate = (xhr) => {
23 // force the XHR2 timeout polyfill
24 xhr.timeout = null;
25 this.requests.push(xhr);
26 };
27
28 // fake timers
29 this.clock = sinon.useFakeTimers();
30 videojs.xhr.XMLHttpRequest = this.sinonXhr;
31
32 this.fakeHls = { 21 this.fakeHls = {
33 xhr: xhrFactory() 22 xhr: xhrFactory()
34 }; 23 };
35 }, 24 },
36 afterEach() { 25 afterEach() {
37 this.sinonXhr.restore(); 26 this.env.restore();
38 this.clock.restore();
39 videojs.xhr.XMLHttpRequest = this.oldXHR;
40 } 27 }
41 }); 28 });
42 29
...@@ -52,12 +39,16 @@ QUnit.test('throws if the playlist url is empty or undefined', function() { ...@@ -52,12 +39,16 @@ QUnit.test('throws if the playlist url is empty or undefined', function() {
52 QUnit.test('starts without any metadata', function() { 39 QUnit.test('starts without any metadata', function() {
53 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 40 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
54 41
42 loader.load();
43
55 QUnit.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); 44 QUnit.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
56 }); 45 });
57 46
58 QUnit.test('starts with no expired time', function() { 47 QUnit.test('starts with no expired time', function() {
59 let loader = new PlaylistLoader('media.m3u8', this.fakeHls); 48 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
60 49
50 loader.load();
51
61 this.requests.pop().respond(200, null, 52 this.requests.pop().respond(200, null,
62 '#EXTM3U\n' + 53 '#EXTM3U\n' +
63 '#EXTINF:10,\n' + 54 '#EXTINF:10,\n' +
...@@ -68,9 +59,9 @@ QUnit.test('starts with no expired time', function() { ...@@ -68,9 +59,9 @@ QUnit.test('starts with no expired time', function() {
68 }); 59 });
69 60
70 QUnit.test('requests the initial playlist immediately', function() { 61 QUnit.test('requests the initial playlist immediately', function() {
71 /* eslint-disable no-unused-vars */
72 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 62 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
73 /* eslint-enable no-unused-vars */ 63
64 loader.load();
74 65
75 QUnit.strictEqual(this.requests.length, 1, 'made a request'); 66 QUnit.strictEqual(this.requests.length, 1, 'made a request');
76 QUnit.strictEqual(this.requests[0].url, 67 QUnit.strictEqual(this.requests[0].url,
...@@ -82,6 +73,8 @@ QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() { ...@@ -82,6 +73,8 @@ QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() {
82 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 73 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
83 let state; 74 let state;
84 75
76 loader.load();
77
85 loader.on('loadedplaylist', function() { 78 loader.on('loadedplaylist', function() {
86 state = loader.state; 79 state = loader.state;
87 }); 80 });
...@@ -97,6 +90,8 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func ...@@ -97,6 +90,8 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func
97 let loadedmetadatas = 0; 90 let loadedmetadatas = 0;
98 let loader = new PlaylistLoader('media.m3u8', this.fakeHls); 91 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
99 92
93 loader.load();
94
100 loader.on('loadedmetadata', function() { 95 loader.on('loadedmetadata', function() {
101 loadedmetadatas++; 96 loadedmetadatas++;
102 }); 97 });
...@@ -113,10 +108,156 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func ...@@ -113,10 +108,156 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func
113 QUnit.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); 108 QUnit.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata');
114 }); 109 });
115 110
111 QUnit.test('resolves relative media playlist URIs', function() {
112 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
113
114 loader.load();
115
116 this.requests.shift().respond(200, null,
117 '#EXTM3U\n' +
118 '#EXT-X-STREAM-INF:\n' +
119 'video/media.m3u8\n');
120 QUnit.equal(loader.master.playlists[0].resolvedUri, urlTo('video/media.m3u8'),
121 'resolved media URI');
122 });
123
124 QUnit.test('recognizes absolute URIs and requests them unmodified', function() {
125 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
126
127 loader.load();
128
129 this.requests.shift().respond(200, null,
130 '#EXTM3U\n' +
131 '#EXT-X-STREAM-INF:\n' +
132 'http://example.com/video/media.m3u8\n');
133 QUnit.equal(loader.master.playlists[0].resolvedUri,
134 'http://example.com/video/media.m3u8', 'resolved media URI');
135
136 this.requests.shift().respond(200, null,
137 '#EXTM3U\n' +
138 '#EXTINF:10,\n' +
139 'http://example.com/00001.ts\n' +
140 '#EXT-X-ENDLIST\n');
141 QUnit.equal(loader.media().segments[0].resolvedUri,
142 'http://example.com/00001.ts', 'resolved segment URI');
143 });
144
145 QUnit.test('recognizes domain-relative URLs', function() {
146 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
147
148 loader.load();
149
150 this.requests.shift().respond(200, null,
151 '#EXTM3U\n' +
152 '#EXT-X-STREAM-INF:\n' +
153 '/media.m3u8\n');
154 QUnit.equal(loader.master.playlists[0].resolvedUri,
155 window.location.protocol + '//' +
156 window.location.host + '/media.m3u8',
157 'resolved media URI');
158
159 this.requests.shift().respond(200, null,
160 '#EXTM3U\n' +
161 '#EXTINF:10,\n' +
162 '/00001.ts\n' +
163 '#EXT-X-ENDLIST\n');
164 QUnit.equal(loader.media().segments[0].resolvedUri,
165 window.location.protocol + '//' +
166 window.location.host + '/00001.ts',
167 'resolved segment URI');
168 });
169
170 QUnit.test('recognizes key URLs relative to master and playlist', function() {
171 let loader = new PlaylistLoader('/video/media-encrypted.m3u8', this.fakeHls);
172
173 loader.load();
174
175 this.requests.shift().respond(200, null,
176 '#EXTM3U\n' +
177 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
178 'playlist/playlist.m3u8\n' +
179 '#EXT-X-ENDLIST\n');
180 QUnit.equal(loader.master.playlists[0].resolvedUri,
181 window.location.protocol + '//' +
182 window.location.host + '/video/playlist/playlist.m3u8',
183 'resolved media URI');
184
185 this.requests.shift().respond(200, null,
186 '#EXTM3U\n' +
187 '#EXT-X-TARGETDURATION:15\n' +
188 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
189 '#EXTINF:2.833,\n' +
190 'http://example.com/000001.ts\n' +
191 '#EXT-X-ENDLIST\n');
192 QUnit.equal(loader.media().segments[0].key.resolvedUri,
193 window.location.protocol + '//' +
194 window.location.host + '/video/playlist/keys/key.php',
195 'resolved multiple relative paths for key URI');
196 });
197
198 QUnit.test('trigger an error event when a media playlist 404s', function() {
199 let count = 0;
200 let loader = new PlaylistLoader('manifest/master.m3u8', this.fakeHls);
201
202 loader.load();
203
204 loader.on('error', function() {
205 count += 1;
206 });
207
208 // master
209 this.requests.shift().respond(200, null,
210 '#EXTM3U\n' +
211 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
212 'playlist/playlist.m3u8\n' +
213 '#EXT-X-STREAM-INF:PROGRAM-ID=2,BANDWIDTH=170\n' +
214 'playlist/playlist2.m3u8\n' +
215 '#EXT-X-ENDLIST\n');
216 QUnit.equal(count, 0,
217 'error not triggered before requesting playlist');
218
219 // playlist
220 this.requests.shift().respond(404);
221
222 QUnit.equal(count, 1,
223 'error triggered after playlist 404');
224 });
225
226 QUnit.test('recognizes absolute key URLs', function() {
227 let loader = new PlaylistLoader('/video/media-encrypted.m3u8', this.fakeHls);
228
229 loader.load();
230
231 this.requests.shift().respond(200, null,
232 '#EXTM3U\n' +
233 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
234 'playlist/playlist.m3u8\n' +
235 '#EXT-X-ENDLIST\n');
236 QUnit.equal(loader.master.playlists[0].resolvedUri,
237 window.location.protocol + '//' +
238 window.location.host + '/video/playlist/playlist.m3u8',
239 'resolved media URI');
240
241 this.requests.shift().respond(
242 200,
243 null,
244 '#EXTM3U\n' +
245 '#EXT-X-TARGETDURATION:15\n' +
246 '#EXT-X-KEY:METHOD=AES-128,URI="http://example.com/keys/key.php"\n' +
247 '#EXTINF:2.833,\n' +
248 'http://example.com/000001.ts\n' +
249 '#EXT-X-ENDLIST\n'
250 );
251 QUnit.equal(loader.media().segments[0].key.resolvedUri,
252 'http://example.com/keys/key.php', 'resolved absolute path for key URI');
253 });
254
116 QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist', 255 QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist',
117 function() { 256 function() {
118 let loader = new PlaylistLoader('media.m3u8', this.fakeHls); 257 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
119 258
259 loader.load();
260
120 this.requests.pop().respond(200, null, 261 this.requests.pop().respond(200, null,
121 '#EXTM3U\n' + 262 '#EXTM3U\n' +
122 '#EXTINF:10,\n' + 263 '#EXTINF:10,\n' +
...@@ -131,6 +272,8 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { ...@@ -131,6 +272,8 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() {
131 let loadedMetadata = 0; 272 let loadedMetadata = 0;
132 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 273 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
133 274
275 loader.load();
276
134 loader.on('loadedplaylist', function() { 277 loader.on('loadedplaylist', function() {
135 loadedPlaylist++; 278 loadedPlaylist++;
136 }); 279 });
...@@ -164,6 +307,8 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { ...@@ -164,6 +307,8 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() {
164 QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { 307 QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() {
165 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 308 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
166 309
310 loader.load();
311
167 this.requests.pop().respond(200, null, 312 this.requests.pop().respond(200, null,
168 '#EXTM3U\n' + 313 '#EXTM3U\n' +
169 '#EXTINF:10,\n' + 314 '#EXTINF:10,\n' +
...@@ -180,6 +325,8 @@ QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', functi ...@@ -180,6 +325,8 @@ QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', functi
180 QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() { 325 QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() {
181 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 326 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
182 327
328 loader.load();
329
183 this.requests.pop().respond(200, null, 330 this.requests.pop().respond(200, null,
184 '#EXTM3U\n' + 331 '#EXTM3U\n' +
185 '#EXTINF:10,\n' + 332 '#EXTINF:10,\n' +
...@@ -197,6 +344,8 @@ QUnit.test('does not increment expired seconds before firstplay is triggered', ...@@ -197,6 +344,8 @@ QUnit.test('does not increment expired seconds before firstplay is triggered',
197 function() { 344 function() {
198 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 345 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
199 346
347 loader.load();
348
200 this.requests.pop().respond(200, null, 349 this.requests.pop().respond(200, null,
201 '#EXTM3U\n' + 350 '#EXTM3U\n' +
202 '#EXT-X-MEDIA-SEQUENCE:0\n' + 351 '#EXT-X-MEDIA-SEQUENCE:0\n' +
...@@ -227,6 +376,8 @@ function() { ...@@ -227,6 +376,8 @@ function() {
227 QUnit.test('increments expired seconds after a segment is removed', function() { 376 QUnit.test('increments expired seconds after a segment is removed', function() {
228 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 377 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
229 378
379 loader.load();
380
230 loader.trigger('firstplay'); 381 loader.trigger('firstplay');
231 this.requests.pop().respond(200, null, 382 this.requests.pop().respond(200, null,
232 '#EXTM3U\n' + 383 '#EXTM3U\n' +
...@@ -258,6 +409,8 @@ QUnit.test('increments expired seconds after a segment is removed', function() { ...@@ -258,6 +409,8 @@ QUnit.test('increments expired seconds after a segment is removed', function() {
258 QUnit.test('increments expired seconds after a discontinuity', function() { 409 QUnit.test('increments expired seconds after a discontinuity', function() {
259 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 410 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
260 411
412 loader.load();
413
261 loader.trigger('firstplay'); 414 loader.trigger('firstplay');
262 this.requests.pop().respond(200, null, 415 this.requests.pop().respond(200, null,
263 '#EXTM3U\n' + 416 '#EXTM3U\n' +
...@@ -306,6 +459,8 @@ QUnit.test('tracks expired seconds properly when two discontinuities expire at o ...@@ -306,6 +459,8 @@ QUnit.test('tracks expired seconds properly when two discontinuities expire at o
306 function() { 459 function() {
307 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 460 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
308 461
462 loader.load();
463
309 loader.trigger('firstplay'); 464 loader.trigger('firstplay');
310 this.requests.pop().respond(200, null, 465 this.requests.pop().respond(200, null,
311 '#EXTM3U\n' + 466 '#EXTM3U\n' +
...@@ -334,6 +489,8 @@ QUnit.test('estimates expired if an entire window elapses between live playlist ...@@ -334,6 +489,8 @@ QUnit.test('estimates expired if an entire window elapses between live playlist
334 function() { 489 function() {
335 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 490 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
336 491
492 loader.load();
493
337 loader.trigger('firstplay'); 494 loader.trigger('firstplay');
338 this.requests.pop().respond(200, null, 495 this.requests.pop().respond(200, null,
339 '#EXTM3U\n' + 496 '#EXTM3U\n' +
...@@ -361,6 +518,8 @@ QUnit.test('emits an error when an initial playlist request fails', function() { ...@@ -361,6 +518,8 @@ QUnit.test('emits an error when an initial playlist request fails', function() {
361 let errors = []; 518 let errors = [];
362 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 519 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
363 520
521 loader.load();
522
364 loader.on('error', function() { 523 loader.on('error', function() {
365 errors.push(loader.error); 524 errors.push(loader.error);
366 }); 525 });
...@@ -374,6 +533,8 @@ QUnit.test('errors when an initial media playlist request fails', function() { ...@@ -374,6 +533,8 @@ QUnit.test('errors when an initial media playlist request fails', function() {
374 let errors = []; 533 let errors = [];
375 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 534 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
376 535
536 loader.load();
537
377 loader.on('error', function() { 538 loader.on('error', function() {
378 errors.push(loader.error); 539 errors.push(loader.error);
379 }); 540 });
...@@ -393,9 +554,9 @@ QUnit.test('errors when an initial media playlist request fails', function() { ...@@ -393,9 +554,9 @@ QUnit.test('errors when an initial media playlist request fails', function() {
393 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 554 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
394 QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload', 555 QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload',
395 function() { 556 function() {
396 /* eslint-disable no-unused-vars */
397 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 557 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
398 /* eslint-enable no-unused-vars */ 558
559 loader.load();
399 560
400 this.requests.pop().respond(200, null, 561 this.requests.pop().respond(200, null,
401 '#EXTM3U\n' + 562 '#EXTM3U\n' +
...@@ -422,6 +583,8 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() { ...@@ -422,6 +583,8 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() {
422 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 583 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
423 let segment; 584 let segment;
424 585
586 loader.load();
587
425 this.requests.pop().respond(200, null, 588 this.requests.pop().respond(200, null,
426 '#EXTM3U\n' + 589 '#EXTM3U\n' +
427 '#EXT-X-MEDIA-SEQUENCE:0\n' + 590 '#EXT-X-MEDIA-SEQUENCE:0\n' +
...@@ -454,6 +617,8 @@ QUnit.test('clears the update timeout when switching quality', function() { ...@@ -454,6 +617,8 @@ QUnit.test('clears the update timeout when switching quality', function() {
454 let loader = new PlaylistLoader('live-master.m3u8', this.fakeHls); 617 let loader = new PlaylistLoader('live-master.m3u8', this.fakeHls);
455 let refreshes = 0; 618 let refreshes = 0;
456 619
620 loader.load();
621
457 // track the number of playlist refreshes triggered 622 // track the number of playlist refreshes triggered
458 loader.on('mediaupdatetimeout', function() { 623 loader.on('mediaupdatetimeout', function() {
459 refreshes++; 624 refreshes++;
...@@ -485,9 +650,9 @@ QUnit.test('clears the update timeout when switching quality', function() { ...@@ -485,9 +650,9 @@ QUnit.test('clears the update timeout when switching quality', function() {
485 }); 650 });
486 651
487 QUnit.test('media-sequence updates are considered a playlist change', function() { 652 QUnit.test('media-sequence updates are considered a playlist change', function() {
488 /* eslint-disable no-unused-vars */
489 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 653 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
490 /* eslint-enable no-unused-vars */ 654
655 loader.load();
491 656
492 this.requests.pop().respond(200, null, 657 this.requests.pop().respond(200, null,
493 '#EXTM3U\n' + 658 '#EXTM3U\n' +
...@@ -512,6 +677,8 @@ QUnit.test('emits an error if a media refresh fails', function() { ...@@ -512,6 +677,8 @@ QUnit.test('emits an error if a media refresh fails', function() {
512 let errorResponseText = 'custom error message'; 677 let errorResponseText = 'custom error message';
513 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 678 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
514 679
680 loader.load();
681
515 loader.on('error', function() { 682 loader.on('error', function() {
516 errors++; 683 errors++;
517 }); 684 });
...@@ -534,6 +701,8 @@ QUnit.test('emits an error if a media refresh fails', function() { ...@@ -534,6 +701,8 @@ QUnit.test('emits an error if a media refresh fails', function() {
534 QUnit.test('switches media playlists when requested', function() { 701 QUnit.test('switches media playlists when requested', function() {
535 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 702 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
536 703
704 loader.load();
705
537 this.requests.pop().respond(200, null, 706 this.requests.pop().respond(200, null,
538 '#EXTM3U\n' + 707 '#EXTM3U\n' +
539 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 708 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -563,6 +732,8 @@ QUnit.test('switches media playlists when requested', function() { ...@@ -563,6 +732,8 @@ QUnit.test('switches media playlists when requested', function() {
563 QUnit.test('can switch playlists immediately after the master is downloaded', function() { 732 QUnit.test('can switch playlists immediately after the master is downloaded', function() {
564 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 733 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
565 734
735 loader.load();
736
566 loader.on('loadedplaylist', function() { 737 loader.on('loadedplaylist', function() {
567 loader.media('high.m3u8'); 738 loader.media('high.m3u8');
568 }); 739 });
...@@ -578,6 +749,8 @@ QUnit.test('can switch playlists immediately after the master is downloaded', fu ...@@ -578,6 +749,8 @@ QUnit.test('can switch playlists immediately after the master is downloaded', fu
578 QUnit.test('can switch media playlists based on URI', function() { 749 QUnit.test('can switch media playlists based on URI', function() {
579 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 750 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
580 751
752 loader.load();
753
581 this.requests.pop().respond(200, null, 754 this.requests.pop().respond(200, null,
582 '#EXTM3U\n' + 755 '#EXTM3U\n' +
583 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 756 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -607,6 +780,8 @@ QUnit.test('can switch media playlists based on URI', function() { ...@@ -607,6 +780,8 @@ QUnit.test('can switch media playlists based on URI', function() {
607 QUnit.test('aborts in-flight playlist refreshes when switching', function() { 780 QUnit.test('aborts in-flight playlist refreshes when switching', function() {
608 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 781 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
609 782
783 loader.load();
784
610 this.requests.pop().respond(200, null, 785 this.requests.pop().respond(200, null,
611 '#EXTM3U\n' + 786 '#EXTM3U\n' +
612 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 787 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -629,6 +804,8 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function() { ...@@ -629,6 +804,8 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function() {
629 QUnit.test('switching to the active playlist is a no-op', function() { 804 QUnit.test('switching to the active playlist is a no-op', function() {
630 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 805 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
631 806
807 loader.load();
808
632 this.requests.pop().respond(200, null, 809 this.requests.pop().respond(200, null,
633 '#EXTM3U\n' + 810 '#EXTM3U\n' +
634 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 811 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -649,6 +826,8 @@ QUnit.test('switching to the active playlist is a no-op', function() { ...@@ -649,6 +826,8 @@ QUnit.test('switching to the active playlist is a no-op', function() {
649 QUnit.test('switching to the active live playlist is a no-op', function() { 826 QUnit.test('switching to the active live playlist is a no-op', function() {
650 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 827 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
651 828
829 loader.load();
830
652 this.requests.pop().respond(200, null, 831 this.requests.pop().respond(200, null,
653 '#EXTM3U\n' + 832 '#EXTM3U\n' +
654 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 833 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -668,6 +847,8 @@ QUnit.test('switching to the active live playlist is a no-op', function() { ...@@ -668,6 +847,8 @@ QUnit.test('switching to the active live playlist is a no-op', function() {
668 QUnit.test('switches back to loaded playlists without re-requesting them', function() { 847 QUnit.test('switches back to loaded playlists without re-requesting them', function() {
669 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 848 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
670 849
850 loader.load();
851
671 this.requests.pop().respond(200, null, 852 this.requests.pop().respond(200, null,
672 '#EXTM3U\n' + 853 '#EXTM3U\n' +
673 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 854 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -697,6 +878,8 @@ QUnit.test('aborts outstanding requests if switching back to an already loaded p ...@@ -697,6 +878,8 @@ QUnit.test('aborts outstanding requests if switching back to an already loaded p
697 function() { 878 function() {
698 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 879 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
699 880
881 loader.load();
882
700 this.requests.pop().respond(200, null, 883 this.requests.pop().respond(200, null,
701 '#EXTM3U\n' + 884 '#EXTM3U\n' +
702 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 885 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -731,6 +914,8 @@ QUnit.test('does not abort requests when the same playlist is re-requested', ...@@ -731,6 +914,8 @@ QUnit.test('does not abort requests when the same playlist is re-requested',
731 function() { 914 function() {
732 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 915 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
733 916
917 loader.load();
918
734 this.requests.pop().respond(200, null, 919 this.requests.pop().respond(200, null,
735 '#EXTM3U\n' + 920 '#EXTM3U\n' +
736 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 921 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -753,6 +938,8 @@ function() { ...@@ -753,6 +938,8 @@ function() {
753 QUnit.test('throws an error if a media switch is initiated too early', function() { 938 QUnit.test('throws an error if a media switch is initiated too early', function() {
754 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 939 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
755 940
941 loader.load();
942
756 QUnit.throws(function() { 943 QUnit.throws(function() {
757 loader.media('high.m3u8'); 944 loader.media('high.m3u8');
758 }, 'threw an error from HAVE_NOTHING'); 945 }, 'threw an error from HAVE_NOTHING');
...@@ -769,6 +956,8 @@ QUnit.test('throws an error if a switch to an unrecognized playlist is requested ...@@ -769,6 +956,8 @@ QUnit.test('throws an error if a switch to an unrecognized playlist is requested
769 function() { 956 function() {
770 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 957 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
771 958
959 loader.load();
960
772 this.requests.pop().respond(200, null, 961 this.requests.pop().respond(200, null,
773 '#EXTM3U\n' + 962 '#EXTM3U\n' +
774 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 963 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -782,6 +971,8 @@ function() { ...@@ -782,6 +971,8 @@ function() {
782 QUnit.test('dispose cancels the refresh timeout', function() { 971 QUnit.test('dispose cancels the refresh timeout', function() {
783 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 972 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
784 973
974 loader.load();
975
785 this.requests.pop().respond(200, null, 976 this.requests.pop().respond(200, null,
786 '#EXTM3U\n' + 977 '#EXTM3U\n' +
787 '#EXT-X-MEDIA-SEQUENCE:0\n' + 978 '#EXT-X-MEDIA-SEQUENCE:0\n' +
...@@ -797,6 +988,8 @@ QUnit.test('dispose cancels the refresh timeout', function() { ...@@ -797,6 +988,8 @@ QUnit.test('dispose cancels the refresh timeout', function() {
797 QUnit.test('dispose aborts pending refresh requests', function() { 988 QUnit.test('dispose aborts pending refresh requests', function() {
798 let loader = new PlaylistLoader('live.m3u8', this.fakeHls); 989 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
799 990
991 loader.load();
992
800 this.requests.pop().respond(200, null, 993 this.requests.pop().respond(200, null,
801 '#EXTM3U\n' + 994 '#EXTM3U\n' +
802 '#EXT-X-MEDIA-SEQUENCE:0\n' + 995 '#EXT-X-MEDIA-SEQUENCE:0\n' +
...@@ -815,6 +1008,8 @@ QUnit.test('errors if requests take longer than 45s', function() { ...@@ -815,6 +1008,8 @@ QUnit.test('errors if requests take longer than 45s', function() {
815 let loader = new PlaylistLoader('media.m3u8', this.fakeHls); 1008 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
816 let errors = 0; 1009 let errors = 0;
817 1010
1011 loader.load();
1012
818 loader.on('error', function() { 1013 loader.on('error', function() {
819 errors++; 1014 errors++;
820 }); 1015 });
...@@ -827,10 +1022,16 @@ QUnit.test('errors if requests take longer than 45s', function() { ...@@ -827,10 +1022,16 @@ QUnit.test('errors if requests take longer than 45s', function() {
827 QUnit.test('triggers an event when the active media changes', function() { 1022 QUnit.test('triggers an event when the active media changes', function() {
828 let loader = new PlaylistLoader('master.m3u8', this.fakeHls); 1023 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
829 let mediaChanges = 0; 1024 let mediaChanges = 0;
1025 let mediaChangings = 0;
1026
1027 loader.load();
830 1028
831 loader.on('mediachange', function() { 1029 loader.on('mediachange', function() {
832 mediaChanges++; 1030 mediaChanges++;
833 }); 1031 });
1032 loader.on('mediachanging', function() {
1033 mediaChangings++;
1034 });
834 this.requests.pop().respond(200, null, 1035 this.requests.pop().respond(200, null,
835 '#EXTM3U\n' + 1036 '#EXTM3U\n' +
836 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + 1037 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
...@@ -843,9 +1044,11 @@ QUnit.test('triggers an event when the active media changes', function() { ...@@ -843,9 +1044,11 @@ QUnit.test('triggers an event when the active media changes', function() {
843 '#EXTINF:10,\n' + 1044 '#EXTINF:10,\n' +
844 'low-0.ts\n' + 1045 'low-0.ts\n' +
845 '#EXT-X-ENDLIST\n'); 1046 '#EXT-X-ENDLIST\n');
1047 QUnit.strictEqual(mediaChangings, 0, 'initial selection is not a media changing');
846 QUnit.strictEqual(mediaChanges, 0, 'initial selection is not a media change'); 1048 QUnit.strictEqual(mediaChanges, 0, 'initial selection is not a media change');
847 1049
848 loader.media('high.m3u8'); 1050 loader.media('high.m3u8');
1051 QUnit.strictEqual(mediaChangings, 1, 'mediachanging fires immediately');
849 QUnit.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately'); 1052 QUnit.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately');
850 1053
851 this.requests.shift().respond(200, null, 1054 this.requests.shift().respond(200, null,
...@@ -854,166 +1057,25 @@ QUnit.test('triggers an event when the active media changes', function() { ...@@ -854,166 +1057,25 @@ QUnit.test('triggers an event when the active media changes', function() {
854 '#EXTINF:10,\n' + 1057 '#EXTINF:10,\n' +
855 'high-0.ts\n' + 1058 'high-0.ts\n' +
856 '#EXT-X-ENDLIST\n'); 1059 '#EXT-X-ENDLIST\n');
1060 QUnit.strictEqual(mediaChangings, 1, 'still one mediachanging');
857 QUnit.strictEqual(mediaChanges, 1, 'fired a mediachange'); 1061 QUnit.strictEqual(mediaChanges, 1, 'fired a mediachange');
858 1062
859 // switch back to an already loaded playlist 1063 // switch back to an already loaded playlist
860 loader.media('low.m3u8'); 1064 loader.media('low.m3u8');
1065 QUnit.strictEqual(mediaChangings, 2, 'mediachanging fires');
861 QUnit.strictEqual(mediaChanges, 2, 'fired a mediachange'); 1066 QUnit.strictEqual(mediaChanges, 2, 'fired a mediachange');
862 1067
863 // trigger a no-op switch 1068 // trigger a no-op switch
864 loader.media('low.m3u8'); 1069 loader.media('low.m3u8');
1070 QUnit.strictEqual(mediaChangings, 2, 'mediachanging ignored the no-op');
865 QUnit.strictEqual(mediaChanges, 2, 'ignored a no-op media change'); 1071 QUnit.strictEqual(mediaChanges, 2, 'ignored a no-op media change');
866 }); 1072 });
867 1073
868 QUnit.test('can get media index by playback position for non-live videos', function() {
869 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
870
871 this.requests.shift().respond(200, null,
872 '#EXTM3U\n' +
873 '#EXT-X-MEDIA-SEQUENCE:0\n' +
874 '#EXTINF:4,\n' +
875 '0.ts\n' +
876 '#EXTINF:5,\n' +
877 '1.ts\n' +
878 '#EXTINF:6,\n' +
879 '2.ts\n' +
880 '#EXT-X-ENDLIST\n');
881
882 QUnit.equal(loader.getMediaIndexForTime_(-1),
883 0,
884 'the index is never less than zero');
885 QUnit.equal(loader.getMediaIndexForTime_(0), 0, 'time zero is index zero');
886 QUnit.equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero');
887 QUnit.equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2');
888 QUnit.equal(loader.getMediaIndexForTime_(22),
889 2,
890 'time greater than the length is index 2');
891 });
892
893 QUnit.test('returns the lower index when calculating for a segment boundary', function() {
894 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
895
896 this.requests.shift().respond(200, null,
897 '#EXTM3U\n' +
898 '#EXT-X-MEDIA-SEQUENCE:0\n' +
899 '#EXTINF:4,\n' +
900 '0.ts\n' +
901 '#EXTINF:5,\n' +
902 '1.ts\n' +
903 '#EXT-X-ENDLIST\n');
904 QUnit.equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches');
905 QUnit.equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down');
906 QUnit.equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5');
907 });
908
909 QUnit.test('accounts for non-zero starting segment time when calculating media index',
910 function() {
911 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
912
913 this.requests.shift().respond(200, null,
914 '#EXTM3U\n' +
915 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
916 '#EXTINF:4,\n' +
917 '1001.ts\n' +
918 '#EXTINF:5,\n' +
919 '1002.ts\n');
920 loader.media().segments[0].end = 154;
921
922 QUnit.equal(loader.getMediaIndexForTime_(0),
923 -1,
924 'the lowest returned value is negative one');
925 QUnit.equal(loader.getMediaIndexForTime_(45),
926 -1,
927 'expired content returns negative one');
928 QUnit.equal(loader.getMediaIndexForTime_(75),
929 -1,
930 'expired content returns negative one');
931 QUnit.equal(loader.getMediaIndexForTime_(50 + 100),
932 0,
933 'calculates the earliest available position');
934 QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2),
935 0,
936 'calculates within the first segment');
937 QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4),
938 1,
939 'calculates within the second segment');
940 QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5),
941 1,
942 'calculates within the second segment');
943 QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 6),
944 1,
945 'calculates within the second segment');
946 });
947
948 QUnit.test('prefers precise segment timing when tracking expired time', function() {
949 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
950
951 loader.trigger('firstplay');
952 this.requests.shift().respond(200, null,
953 '#EXTM3U\n' +
954 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
955 '#EXTINF:4,\n' +
956 '1001.ts\n' +
957 '#EXTINF:5,\n' +
958 '1002.ts\n');
959 // setup the loader with an "imprecise" value as if it had been
960 // accumulating segment durations as they expire
961 loader.expired_ = 160;
962 // annotate the first segment with a start time
963 // this number would be coming from the Source Buffer in practice
964 loader.media().segments[0].end = 150;
965
966 QUnit.equal(loader.getMediaIndexForTime_(149),
967 0,
968 'prefers the value on the first segment');
969
970 // trigger a playlist refresh
971 this.clock.tick(10 * 1000);
972 this.requests.shift().respond(200, null,
973 '#EXTM3U\n' +
974 '#EXT-X-MEDIA-SEQUENCE:1002\n' +
975 '#EXTINF:5,\n' +
976 '1002.ts\n');
977 QUnit.equal(loader.getMediaIndexForTime_(150 + 4 + 1),
978 0,
979 'tracks precise expired times');
980 });
981
982 QUnit.test('accounts for expired time when calculating media index', function() {
983 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
984
985 this.requests.shift().respond(200, null,
986 '#EXTM3U\n' +
987 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
988 '#EXTINF:4,\n' +
989 '1001.ts\n' +
990 '#EXTINF:5,\n' +
991 '1002.ts\n');
992 loader.expired_ = 150;
993
994 QUnit.equal(loader.getMediaIndexForTime_(0),
995 -1,
996 'expired content returns a negative index');
997 QUnit.equal(loader.getMediaIndexForTime_(75),
998 -1,
999 'expired content returns a negative index');
1000 QUnit.equal(loader.getMediaIndexForTime_(50 + 100),
1001 0,
1002 'calculates the earliest available position');
1003 QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2),
1004 0,
1005 'calculates within the first segment');
1006 QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5),
1007 1,
1008 'calculates within the second segment');
1009 QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 6),
1010 1,
1011 'calculates within the second segment');
1012 });
1013
1014 QUnit.test('does not misintrepret playlists missing newlines at the end', function() { 1074 QUnit.test('does not misintrepret playlists missing newlines at the end', function() {
1015 let loader = new PlaylistLoader('media.m3u8', this.fakeHls); 1075 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
1016 1076
1077 loader.load();
1078
1017 // no newline 1079 // no newline
1018 this.requests.shift().respond(200, null, 1080 this.requests.shift().respond(200, null,
1019 '#EXTM3U\n' + 1081 '#EXTM3U\n' +
......
1 import Playlist from '../src/playlist'; 1 import Playlist from '../src/playlist';
2 import PlaylistLoader from '../src/playlist-loader';
2 import QUnit from 'qunit'; 3 import QUnit from 'qunit';
4 import xhrFactory from '../src/xhr';
5 import { useFakeEnvironment } from './test-helpers';
6
3 QUnit.module('Playlist Duration'); 7 QUnit.module('Playlist Duration');
4 8
5 QUnit.test('total duration for live playlists is Infinity', function() { 9 QUnit.test('total duration for live playlists is Infinity', function() {
...@@ -376,3 +380,251 @@ QUnit.test('seekable end accounts for non-standard target durations', function() ...@@ -376,3 +380,251 @@ QUnit.test('seekable end accounts for non-standard target durations', function()
376 9 - (2 + 2 + 1), 380 9 - (2 + 2 + 1),
377 'allows seeking no further than three segments from the end'); 381 'allows seeking no further than three segments from the end');
378 }); 382 });
383
384 QUnit.module('Playlist Media Index For Time', {
385 beforeEach() {
386 this.env = useFakeEnvironment();
387 this.clock = this.env.clock;
388 this.requests = this.env.requests;
389 this.fakeHls = {
390 xhr: xhrFactory()
391 };
392 },
393 afterEach() {
394 this.env.restore();
395 }
396 });
397
398 QUnit.test('can get media index by playback position for non-live videos', function() {
399 let media;
400 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
401
402 loader.load();
403
404 this.requests.shift().respond(200, null,
405 '#EXTM3U\n' +
406 '#EXT-X-MEDIA-SEQUENCE:0\n' +
407 '#EXTINF:4,\n' +
408 '0.ts\n' +
409 '#EXTINF:5,\n' +
410 '1.ts\n' +
411 '#EXTINF:6,\n' +
412 '2.ts\n' +
413 '#EXT-X-ENDLIST\n'
414 );
415
416 media = loader.media();
417
418 QUnit.equal(Playlist.getMediaIndexForTime_(media, -1), 0,
419 'the index is never less than zero');
420 QUnit.equal(Playlist.getMediaIndexForTime_(media, 0), 0, 'time zero is index zero');
421 QUnit.equal(Playlist.getMediaIndexForTime_(media, 3), 0, 'time three is index zero');
422 QUnit.equal(Playlist.getMediaIndexForTime_(media, 10), 2, 'time 10 is index 2');
423 QUnit.equal(Playlist.getMediaIndexForTime_(media, 22), 2,
424 'time greater than the length is index 2');
425 });
426
427 QUnit.test('returns the lower index when calculating for a segment boundary', function() {
428 let media;
429 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
430
431 loader.load();
432
433 this.requests.shift().respond(200, null,
434 '#EXTM3U\n' +
435 '#EXT-X-MEDIA-SEQUENCE:0\n' +
436 '#EXTINF:4,\n' +
437 '0.ts\n' +
438 '#EXTINF:5,\n' +
439 '1.ts\n' +
440 '#EXT-X-ENDLIST\n'
441 );
442
443 media = loader.media();
444
445 QUnit.equal(Playlist.getMediaIndexForTime_(media, 4), 1, 'rounds up exact matches');
446 QUnit.equal(Playlist.getMediaIndexForTime_(media, 3.7), 0, 'rounds down');
447 QUnit.equal(Playlist.getMediaIndexForTime_(media, 4.5), 1, 'rounds up at 0.5');
448 });
449
450 QUnit.test(
451 'accounts for non-zero starting segment time when calculating media index',
452 function() {
453 let media;
454 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
455
456 loader.load();
457
458 this.requests.shift().respond(200, null,
459 '#EXTM3U\n' +
460 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
461 '#EXTINF:4,\n' +
462 '1001.ts\n' +
463 '#EXTINF:5,\n' +
464 '1002.ts\n'
465 );
466 loader.media().segments[0].end = 154;
467
468 media = loader.media();
469
470 QUnit.equal(
471 Playlist.getMediaIndexForTime_(media, 0),
472 -1,
473 'the lowest returned value is negative one'
474 );
475 QUnit.equal(
476 Playlist.getMediaIndexForTime_(media, 45),
477 -1,
478 'expired content returns negative one'
479 );
480 QUnit.equal(
481 Playlist.getMediaIndexForTime_(media, 75),
482 -1,
483 'expired content returns negative one'
484 );
485 QUnit.equal(
486 Playlist.getMediaIndexForTime_(media, 50 + 100),
487 0,
488 'calculates the earliest available position'
489 );
490 QUnit.equal(
491 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2),
492 0,
493 'calculates within the first segment'
494 );
495 QUnit.equal(
496 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2),
497 0,
498 'calculates within the first segment'
499 );
500 QUnit.equal(
501 Playlist.getMediaIndexForTime_(media, 50 + 100 + 4),
502 1,
503 'calculates within the second segment'
504 );
505 QUnit.equal(
506 Playlist.getMediaIndexForTime_(media, 50 + 100 + 4.5),
507 1,
508 'calculates within the second segment'
509 );
510 QUnit.equal(
511 Playlist.getMediaIndexForTime_(media, 50 + 100 + 6),
512 1,
513 'calculates within the second segment'
514 );
515
516 loader.media().segments[1].end = 159;
517 QUnit.equal(
518 Playlist.getMediaIndexForTime_(media, 159),
519 2,
520 'returns number of segments when time is equal to end of last segment'
521 );
522 QUnit.equal(
523 Playlist.getMediaIndexForTime_(media, 159.1),
524 2,
525 'returns number of segments when time is past end of last segment'
526 );
527 });
528
529 QUnit.test('prefers precise segment timing when tracking expired time', function() {
530 let media;
531 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
532
533 loader.load();
534
535 loader.trigger('firstplay');
536 this.requests.shift().respond(200, null,
537 '#EXTM3U\n' +
538 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
539 '#EXTINF:4,\n' +
540 '1001.ts\n' +
541 '#EXTINF:5,\n' +
542 '1002.ts\n'
543 );
544 // setup the loader with an "imprecise" value as if it had been
545 // accumulating segment durations as they expire
546 loader.expired_ = 160;
547 // annotate the first segment with a start time
548 // this number would be coming from the Source Buffer in practice
549 loader.media().segments[0].end = 150;
550
551 media = loader.media();
552
553 QUnit.equal(
554 Playlist.getMediaIndexForTime_(media, 149),
555 0,
556 'prefers the value on the first segment'
557 );
558
559 // trigger a playlist refresh
560 this.clock.tick(10 * 1000);
561 this.requests.shift().respond(200, null,
562 '#EXTM3U\n' +
563 '#EXT-X-MEDIA-SEQUENCE:1002\n' +
564 '#EXTINF:5,\n' +
565 '1002.ts\n'
566 );
567
568 media = loader.media();
569
570 QUnit.equal(
571 Playlist.getMediaIndexForTime_(media, 150 + 4 + 1),
572 0,
573 'tracks precise expired times'
574 );
575 });
576
577 QUnit.test('accounts for expired time when calculating media index', function() {
578 let media;
579 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
580 let expired = 150;
581
582 loader.load();
583
584 this.requests.shift().respond(200, null,
585 '#EXTM3U\n' +
586 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
587 '#EXTINF:4,\n' +
588 '1001.ts\n' +
589 '#EXTINF:5,\n' +
590 '1002.ts\n'
591 );
592
593 media = loader.media();
594
595 QUnit.equal(
596 Playlist.getMediaIndexForTime_(media, 0, expired),
597 -1,
598 'expired content returns a negative index'
599 );
600 QUnit.equal(
601 Playlist.getMediaIndexForTime_(media, 75, expired),
602 -1,
603 'expired content returns a negative index'
604 );
605 QUnit.equal(
606 Playlist.getMediaIndexForTime_(media, 50 + 100, expired),
607 0,
608 'calculates the earliest available position'
609 );
610 QUnit.equal(
611 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2, expired),
612 0,
613 'calculates within the first segment'
614 );
615 QUnit.equal(
616 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2, expired),
617 0,
618 'calculates within the first segment'
619 );
620 QUnit.equal(
621 Playlist.getMediaIndexForTime_(media, 50 + 100 + 4.5, expired),
622 1,
623 'calculates within the second segment'
624 );
625 QUnit.equal(
626 Playlist.getMediaIndexForTime_(media, 50 + 100 + 6, expired),
627 1,
628 'calculates within the second segment'
629 );
630 });
......
1 import Ranges from '../src/ranges';
2 import {createTimeRanges} from 'video.js';
3 import QUnit from 'qunit';
4
5 QUnit.module('TimeRanges Utilities');
6
7 QUnit.test('finds the overlapping time range', function() {
8 let range = Ranges.findRange(createTimeRanges([[0, 5], [6, 12]]), 3);
9
10 QUnit.equal(range.length, 1, 'found one range');
11 QUnit.equal(range.end(0), 5, 'inside the first buffered region');
12
13 range = Ranges.findRange(createTimeRanges([[0, 5], [6, 12]]), 6);
14 QUnit.equal(range.length, 1, 'found one range');
15 QUnit.equal(range.end(0), 12, 'inside the second buffered region');
16 });
17
18 QUnit.module('Buffer Inpsection');
19
20 QUnit.test('detects time range end-point changed by updates', function() {
21 let edge;
22
23 // Single-range changes
24 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
25 createTimeRanges([[0, 11]]));
26 QUnit.strictEqual(edge, 11, 'detected a forward addition');
27
28 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[5, 10]]),
29 createTimeRanges([[0, 10]]));
30 QUnit.strictEqual(edge, null, 'ignores backward addition');
31
32 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[5, 10]]),
33 createTimeRanges([[0, 11]]));
34 QUnit.strictEqual(edge, 11,
35 'detected a forward addition & ignores a backward addition');
36
37 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
38 createTimeRanges([[0, 9]]));
39 QUnit.strictEqual(edge, null,
40 'ignores a backwards addition resulting from a shrinking range');
41
42 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
43 createTimeRanges([[2, 7]]));
44 QUnit.strictEqual(edge, null,
45 'ignores a forward & backwards addition resulting from a shrinking ' +
46 'range');
47
48 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[2, 10]]),
49 createTimeRanges([[0, 7]]));
50 QUnit.strictEqual(
51 edge,
52 null,
53 'ignores a forward & backwards addition resulting from a range shifted backward'
54 );
55
56 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[2, 10]]),
57 createTimeRanges([[5, 15]]));
58 QUnit.strictEqual(edge, 15,
59 'detected a forwards addition resulting from a range shifted foward');
60
61 // Multiple-range changes
62 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
63 createTimeRanges([[0, 11], [12, 15]]));
64 QUnit.strictEqual(edge, null, 'ignores multiple new forward additions');
65
66 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
67 createTimeRanges([[20, 50]]));
68 QUnit.strictEqual(edge, 50, 'detected a forward addition & ignores range removal');
69
70 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
71 createTimeRanges([[0, 50]]));
72 QUnit.strictEqual(edge, 50, 'detected a forward addition & ignores merges');
73
74 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
75 createTimeRanges([[0, 40]]));
76 QUnit.strictEqual(edge, null, 'ignores merges');
77
78 // Empty input
79 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges(),
80 createTimeRanges([[0, 11]]));
81 QUnit.strictEqual(edge, 11, 'handle an empty original TimeRanges object');
82
83 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 11]]),
84 createTimeRanges());
85 QUnit.strictEqual(edge, null, 'handle an empty update TimeRanges object');
86
87 // Null input
88 edge = Ranges.findSoleUncommonTimeRangesEnd(null, createTimeRanges([[0, 11]]));
89 QUnit.strictEqual(edge, 11, 'treat null original buffer as an empty TimeRanges object');
90
91 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 11]]), null);
92 QUnit.strictEqual(edge, null, 'treat null update buffer as an empty TimeRanges object');
93 });
1 import QUnit from 'qunit';
2 import {GOAL_BUFFER_LENGTH, default as SegmentLoader} from '../src/segment-loader';
3 import videojs from 'video.js';
4 import xhrFactory from '../src/xhr';
5 import { useFakeEnvironment, useFakeMediaSource } from './test-helpers.js';
6
7 const playlistWithDuration = function(time, conf) {
8 let result = {
9 mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0,
10 discontinuityStarts: [],
11 segments: [],
12 endList: true
13 };
14 let count = Math.floor(time / 10);
15 let remainder = time % 10;
16 let i;
17 let isEncrypted = conf && conf.isEncrypted;
18
19 for (i = 0; i < count; i++) {
20 result.segments.push({
21 uri: i + '.ts',
22 resolvedUri: i + '.ts',
23 duration: 10
24 });
25 if (isEncrypted) {
26 result.segments[i].key = {
27 uri: i + '-key.php',
28 resolvedUri: i + '-key.php'
29 };
30 }
31 }
32 if (remainder) {
33 result.segments.push({
34 uri: i + '.ts',
35 duration: remainder
36 });
37 }
38 return result;
39 };
40
41 let currentTime;
42 let mediaSource;
43 let loader;
44
45 QUnit.module('Segment Loader', {
46 beforeEach() {
47 this.env = useFakeEnvironment();
48 this.clock = this.env.clock;
49 this.requests = this.env.requests;
50 this.mse = useFakeMediaSource();
51 this.seekable = {
52 length: 0
53 };
54 this.mimeType = 'video/mp2t';
55 this.fakeHls = {
56 xhr: xhrFactory()
57 };
58
59 currentTime = 0;
60 mediaSource = new videojs.MediaSource();
61 mediaSource.trigger('sourceopen');
62 loader = new SegmentLoader({
63 hls: this.fakeHls,
64 currentTime() {
65 return currentTime;
66 },
67 seekable: () => this.seekable,
68 seeking: () => false,
69 hasPlayed: () => true,
70 mediaSource
71 });
72 },
73 afterEach() {
74 this.env.restore();
75 this.mse.restore();
76 }
77 });
78
79 QUnit.test('fails without required initialization options', function() {
80 /* eslint-disable no-new */
81 QUnit.throws(function() {
82 new SegmentLoader();
83 }, 'requires options');
84 QUnit.throws(function() {
85 new SegmentLoader({});
86 }, 'requires a currentTime callback');
87 QUnit.throws(function() {
88 new SegmentLoader({
89 currentTime() {}
90 });
91 }, 'requires a media source');
92 /* eslint-enable */
93 });
94
95 QUnit.test('load waits until a playlist and mime type are specified to proceed',
96 function() {
97 loader.load();
98 QUnit.equal(loader.state, 'INIT', 'waiting in init');
99 QUnit.equal(loader.paused(), false, 'not paused');
100
101 loader.playlist(playlistWithDuration(10));
102 QUnit.equal(this.requests.length, 0, 'have not made a request yet');
103 loader.mimeType(this.mimeType);
104
105 QUnit.equal(this.requests.length, 1, 'made a request');
106 QUnit.equal(loader.state, 'WAITING', 'transitioned states');
107 });
108
109 QUnit.test('calling mime type and load begins buffering', function() {
110 QUnit.equal(loader.state, 'INIT', 'starts in the init state');
111 loader.playlist(playlistWithDuration(10));
112 QUnit.equal(loader.state, 'INIT', 'starts in the init state');
113 QUnit.ok(loader.paused(), 'starts paused');
114
115 loader.mimeType(this.mimeType);
116 QUnit.equal(loader.state, 'INIT', 'still in the init state');
117 loader.load();
118
119 QUnit.equal(loader.state, 'WAITING', 'moves to the ready state');
120 QUnit.ok(!loader.paused(), 'loading is not paused');
121 QUnit.equal(this.requests.length, 1, 'requested a segment');
122 });
123
124 QUnit.test('calling load is idempotent', function() {
125 loader.playlist(playlistWithDuration(20));
126 loader.mimeType(this.mimeType);
127 loader.load();
128 QUnit.equal(loader.state, 'WAITING', 'moves to the ready state');
129 QUnit.equal(this.requests.length, 1, 'made one request');
130
131 loader.load();
132 QUnit.equal(loader.state, 'WAITING', 'still in the ready state');
133 QUnit.equal(this.requests.length, 1, 'still one request');
134
135 // some time passes and a response is received
136 this.clock.tick(100);
137 this.requests[0].response = new Uint8Array(10).buffer;
138 this.requests.shift().respond(200, null, '');
139 loader.load();
140 QUnit.equal(this.requests.length, 0, 'load has no effect');
141 });
142
143 QUnit.test('calling load should unpause', function() {
144 let sourceBuffer;
145
146 loader.playlist(playlistWithDuration(20));
147 loader.pause();
148
149 loader.mimeType(this.mimeType);
150 sourceBuffer = mediaSource.sourceBuffers[0];
151
152 loader.load();
153 QUnit.equal(loader.paused(), false, 'loading unpauses');
154
155 loader.pause();
156 this.clock.tick(1);
157 this.requests[0].response = new Uint8Array(10).buffer;
158 this.requests.shift().respond(200, null, '');
159
160 QUnit.equal(loader.paused(), true, 'stayed paused');
161 loader.load();
162 QUnit.equal(loader.paused(), false, 'unpaused during processing');
163
164 loader.pause();
165 sourceBuffer.trigger('updateend');
166 QUnit.equal(loader.state, 'READY', 'finished processing');
167 QUnit.ok(loader.paused(), 'stayed paused');
168
169 loader.load();
170 QUnit.equal(loader.paused(), false, 'unpaused');
171 });
172
173 QUnit.test('regularly checks the buffer while unpaused', function() {
174 let sourceBuffer;
175
176 loader.playlist(playlistWithDuration(90));
177 loader.mimeType(this.mimeType);
178 loader.load();
179 sourceBuffer = mediaSource.sourceBuffers[0];
180
181 // fill the buffer
182 this.clock.tick(1);
183 this.requests[0].response = new Uint8Array(10).buffer;
184 this.requests.shift().respond(200, null, '');
185 sourceBuffer.buffered = videojs.createTimeRanges([[
186 0, GOAL_BUFFER_LENGTH
187 ]]);
188 sourceBuffer.trigger('updateend');
189 QUnit.equal(this.requests.length, 0, 'no outstanding requests');
190
191 // play some video to drain the buffer
192 currentTime = GOAL_BUFFER_LENGTH;
193 this.clock.tick(10 * 1000);
194 QUnit.equal(this.requests.length, 1, 'requested another segment');
195 });
196
197 QUnit.test('does not check the buffer while paused', function() {
198 let sourceBuffer;
199
200 loader.playlist(playlistWithDuration(90));
201 loader.mimeType(this.mimeType);
202 loader.load();
203 sourceBuffer = mediaSource.sourceBuffers[0];
204
205 loader.pause();
206 this.clock.tick(1);
207 this.requests[0].response = new Uint8Array(10).buffer;
208 this.requests.shift().respond(200, null, '');
209 sourceBuffer.trigger('updateend');
210
211 this.clock.tick(10 * 1000);
212 QUnit.equal(this.requests.length, 0, 'did not make a request');
213 });
214
215 QUnit.test('calculates bandwidth after downloading a segment', function() {
216 loader.playlist(playlistWithDuration(10));
217 loader.mimeType(this.mimeType);
218 loader.load();
219
220 // some time passes and a response is received
221 this.clock.tick(100);
222 this.requests[0].response = new Uint8Array(10).buffer;
223 this.requests.shift().respond(200, null, '');
224
225 QUnit.equal(loader.bandwidth, (10 / 100) * 8 * 1000, 'calculated bandwidth');
226 QUnit.equal(loader.roundTrip, 100, 'saves request round trip time');
227 QUnit.equal(loader.bytesReceived, 10, 'saves bytes received');
228 });
229
230 QUnit.test('segment request timeouts reset bandwidth', function() {
231 loader.playlist(playlistWithDuration(10));
232 loader.mimeType(this.mimeType);
233 loader.load();
234
235 // a lot of time passes so the request times out
236 this.requests[0].timedout = true;
237 this.clock.tick(100 * 1000);
238
239 QUnit.equal(loader.bandwidth, 1, 'reset bandwidth');
240 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time');
241 });
242
243 QUnit.test('appending a segment triggers progress', function() {
244 let progresses = 0;
245
246 loader.on('progress', function() {
247 progresses++;
248 });
249 loader.playlist(playlistWithDuration(10));
250 loader.mimeType(this.mimeType);
251 loader.load();
252
253 // some time passes and a response is received
254 this.requests[0].response = new Uint8Array(10).buffer;
255 this.requests.shift().respond(200, null, '');
256 mediaSource.sourceBuffers[0].trigger('updateend');
257
258 QUnit.equal(progresses, 1, 'fired progress');
259 });
260
261 QUnit.test('only requests one segment at a time', function() {
262 loader.playlist(playlistWithDuration(10));
263 loader.mimeType(this.mimeType);
264 loader.load();
265
266 // a bunch of time passes without recieving a response
267 this.clock.tick(20 * 1000);
268 QUnit.equal(this.requests.length, 1, 'only one request was made');
269 });
270
271 QUnit.test('only appends one segment at a time', function() {
272 loader.playlist(playlistWithDuration(10));
273 loader.mimeType(this.mimeType);
274 loader.load();
275
276 // some time passes and a segment is received
277 this.clock.tick(100);
278 this.requests[0].response = new Uint8Array(10).buffer;
279 this.requests.shift().respond(200, null, '');
280
281 // a lot of time goes by without "updateend"
282 this.clock.tick(20 * 1000);
283
284 QUnit.equal(mediaSource.sourceBuffers[0].updates_.filter(
285 update => update.append).length, 1, 'only one append');
286 QUnit.equal(this.requests.length, 0, 'only made one request');
287 });
288
289 QUnit.test('adjusts the playlist offset if no buffering progress is made', function() {
290 let sourceBuffer;
291 let playlist;
292
293 playlist = playlistWithDuration(40);
294 playlist.endList = false;
295 loader.playlist(playlist);
296 loader.mimeType(this.mimeType);
297 loader.load();
298 sourceBuffer = mediaSource.sourceBuffers[0];
299
300 // buffer some content and switch playlists on progress
301 this.clock.tick(1);
302 this.requests[0].response = new Uint8Array(10).buffer;
303 this.requests.shift().respond(200, null, '');
304 loader.on('progress', function f() {
305 loader.off('progress', f);
306 // switch playlists
307 playlist = playlistWithDuration(40);
308 playlist.uri = 'alternate.m3u8';
309 playlist.endList = false;
310 loader.playlist(playlist);
311 });
312 sourceBuffer.buffered = videojs.createTimeRanges([[0, 5]]);
313 sourceBuffer.trigger('updateend');
314
315 // the next segment doesn't increase the buffer at all
316 QUnit.equal(this.requests[0].url, '0.ts', 'requested the same segment');
317 this.clock.tick(1);
318 this.requests[0].response = new Uint8Array(10).buffer;
319 this.requests.shift().respond(200, null, '');
320 sourceBuffer.trigger('updateend');
321
322 // so the loader should try the next segment
323 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
324 });
325
326 QUnit.test('never attempt to load a segment that ' +
327 'is greater than 90% buffered', function() {
328 let sourceBuffer;
329 let playlist;
330
331 playlist = playlistWithDuration(40);
332 playlist.endList = false;
333 loader.playlist(playlist);
334 loader.mimeType(this.mimeType);
335 loader.load();
336 sourceBuffer = mediaSource.sourceBuffers[0];
337
338 // buffer some content and switch playlists on progress
339 this.clock.tick(1);
340 this.requests[0].response = new Uint8Array(10).buffer;
341 this.requests.shift().respond(200, null, '');
342 loader.on('progress', function f() {
343 loader.off('progress', f);
344 // switch playlists
345 playlist = playlistWithDuration(40);
346 playlist.uri = 'alternate.m3u8';
347 playlist.endList = false;
348 loader.playlist(playlist);
349 });
350 sourceBuffer.buffered = videojs.createTimeRanges([[0, 9.2]]);
351 sourceBuffer.trigger('updateend');
352
353 // the next segment doesn't increase the buffer at all
354 QUnit.equal(this.requests[0].url, '1.ts', 'requested the next segment');
355 this.clock.tick(1);
356 this.requests[0].response = new Uint8Array(10).buffer;
357 this.requests.shift().respond(200, null, '');
358 sourceBuffer.trigger('updateend');
359
360 // so the loader should try the next segment
361 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
362 });
363
364 QUnit.test('adjusts the playlist offset if no buffering progress is made', function() {
365 let sourceBuffer;
366 let playlist;
367
368 playlist = playlistWithDuration(40);
369 playlist.endList = false;
370 loader.playlist(playlist);
371 loader.mimeType(this.mimeType);
372 loader.load();
373 sourceBuffer = mediaSource.sourceBuffers[0];
374
375 // buffer some content and switch playlists on progress
376 this.clock.tick(1);
377 this.requests[0].response = new Uint8Array(10).buffer;
378 this.requests.shift().respond(200, null, '');
379 loader.on('progress', function f() {
380 loader.off('progress', f);
381 // switch playlists
382 playlist = playlistWithDuration(40);
383 playlist.uri = 'alternate.m3u8';
384 playlist.endList = false;
385 loader.playlist(playlist);
386 });
387 sourceBuffer.buffered = videojs.createTimeRanges([[0, 5]]);
388 sourceBuffer.trigger('updateend');
389
390 // the next segment doesn't increase the buffer at all
391 QUnit.equal(this.requests[0].url, '0.ts', 'requested the same segment');
392 this.clock.tick(1);
393 this.requests[0].response = new Uint8Array(10).buffer;
394 this.requests.shift().respond(200, null, '');
395 sourceBuffer.trigger('updateend');
396
397 // so the loader should try the next segment
398 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
399 });
400
401 QUnit.test('cancels outstanding requests on abort', function() {
402 loader.playlist(playlistWithDuration(20));
403 loader.mimeType(this.mimeType);
404 loader.load();
405 loader.xhr_.segmentXhr.onreadystatechange = function() {
406 throw new Error('onreadystatechange should not be called');
407 };
408
409 loader.abort();
410 QUnit.ok(this.requests[0].aborted, 'aborted the first request');
411 QUnit.equal(this.requests.length, 2, 'started a new request');
412 QUnit.equal(loader.state, 'WAITING', 'back to the waiting state');
413 });
414
415 QUnit.test('abort does not cancel segment processing in progress', function() {
416 loader.playlist(playlistWithDuration(20));
417 loader.mimeType(this.mimeType);
418 loader.load();
419
420 this.requests[0].response = new Uint8Array(10).buffer;
421 this.requests.shift().respond(200, null, '');
422
423 loader.abort();
424 QUnit.equal(loader.state, 'APPENDING', 'still appending');
425 });
426
427 QUnit.test('sets the timestampOffset on timeline change', function() {
428 let playlist = playlistWithDuration(40);
429
430 playlist.discontinuityStarts = [1];
431 playlist.segments[1].timeline = 1;
432 loader.playlist(playlist);
433 loader.mimeType(this.mimeType);
434 loader.load();
435
436 // segment 0
437 this.requests[0].response = new Uint8Array(10).buffer;
438 this.requests.shift().respond(200, null, '');
439 mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
440 mediaSource.sourceBuffers[0].trigger('updateend');
441
442 // segment 1, discontinuity
443 this.requests[0].response = new Uint8Array(10).buffer;
444 this.requests.shift().respond(200, null, '');
445 QUnit.equal(mediaSource.sourceBuffers[0].timestampOffset, 10, 'set timestampOffset');
446 });
447
448 QUnit.test('tracks segment end times as they are buffered', function() {
449 let playlist = playlistWithDuration(20);
450
451 loader.playlist(playlist);
452 loader.mimeType(this.mimeType);
453 loader.load();
454
455 this.requests[0].response = new Uint8Array(10).buffer;
456 this.requests.shift().respond(200, null, '');
457
458 mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([
459 [0, 9.5]
460 ]);
461 mediaSource.sourceBuffers[0].trigger('updateend');
462 QUnit.equal(playlist.segments[0].end, 9.5, 'updated duration');
463 });
464
465 QUnit.test('segment 404s should trigger an error', function() {
466 let errors = [];
467
468 loader.playlist(playlistWithDuration(10));
469 loader.mimeType(this.mimeType);
470 loader.load();
471 loader.on('error', function(error) {
472 errors.push(error);
473 });
474 this.requests.shift().respond(404, null, '');
475
476 QUnit.equal(errors.length, 1, 'triggered an error');
477 QUnit.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
478 QUnit.ok(loader.error().xhr, 'included the request object');
479 QUnit.ok(loader.paused(), 'paused the loader');
480 QUnit.equal(loader.state, 'READY', 'returned to the ready state');
481 });
482
483 QUnit.test('segment 5xx status codes trigger an error', function() {
484 let errors = [];
485
486 loader.playlist(playlistWithDuration(10));
487 loader.mimeType(this.mimeType);
488 loader.load();
489 loader.on('error', function(error) {
490 errors.push(error);
491 });
492 this.requests.shift().respond(500, null, '');
493
494 QUnit.equal(errors.length, 1, 'triggered an error');
495 QUnit.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
496 QUnit.ok(loader.error().xhr, 'included the request object');
497 QUnit.ok(loader.paused(), 'paused the loader');
498 QUnit.equal(loader.state, 'READY', 'returned to the ready state');
499 });
500
501 QUnit.test('fires ended at the end of a playlist', function() {
502 let endOfStreams = 0;
503
504 loader.playlist(playlistWithDuration(10));
505 loader.mimeType(this.mimeType);
506 loader.load();
507 loader.mediaSource_ = {
508 readyState: 'open',
509 sourceBuffers: mediaSource.sourceBuffers,
510 endOfStream() {
511 endOfStreams++;
512 }
513 };
514
515 this.requests[0].response = new Uint8Array(10).buffer;
516 this.requests.shift().respond(200, null, '');
517 mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
518 mediaSource.sourceBuffers[0].trigger('updateend');
519 QUnit.equal(endOfStreams, 1, 'triggered ended');
520 });
521
522 QUnit.test('live playlists do not trigger ended', function() {
523 let endOfStreams = 0;
524 let playlist;
525
526 playlist = playlistWithDuration(10);
527 playlist.endList = false;
528 loader.playlist(playlist);
529 loader.mimeType(this.mimeType);
530 loader.load();
531 loader.mediaSource_ = {
532 readyState: 'open',
533 sourceBuffers: mediaSource.sourceBuffers,
534 endOfStream() {
535 endOfStreams++;
536 }
537 };
538
539 this.requests[0].response = new Uint8Array(10).buffer;
540 this.requests.shift().respond(200, null, '');
541 mediaSource.sourceBuffers[0].buffered = videojs.createTimeRanges([[0, 10]]);
542 mediaSource.sourceBuffers[0].trigger('updateend');
543 QUnit.equal(endOfStreams, 0, 'did not trigger ended');
544 });
545
546 QUnit.test('respects the global withCredentials option', function() {
547 let hlsOptions = videojs.options.hls;
548
549 videojs.options.hls = {
550 withCredentials: true
551 };
552 loader = new SegmentLoader({
553 hls: this.fakeHls,
554 currentTime() {
555 return currentTime;
556 },
557 seekable: () => this.seekable,
558 mediaSource
559 });
560 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
561 loader.mimeType(this.mimeType);
562 loader.load();
563
564 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
565 QUnit.ok(this.requests[0].withCredentials, 'key request used withCredentials');
566 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
567 QUnit.ok(this.requests[1].withCredentials, 'segment request used withCredentials');
568 videojs.options.hls = hlsOptions;
569 });
570
571 QUnit.test('respects the withCredentials option', function() {
572 loader = new SegmentLoader({
573 hls: this.fakeHls,
574 currentTime() {
575 return currentTime;
576 },
577 seekable: () => this.seekable,
578 mediaSource,
579 withCredentials: true
580 });
581 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
582 loader.mimeType(this.mimeType);
583 loader.load();
584
585 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
586 QUnit.ok(this.requests[0].withCredentials, 'key request used withCredentials');
587 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
588 QUnit.ok(this.requests[1].withCredentials, 'segment request used withCredentials');
589 });
590
591 QUnit.test('the withCredentials option overrides the global', function() {
592 let hlsOptions = videojs.options.hls;
593
594 videojs.options.hls = {
595 withCredentials: true
596 };
597 loader = new SegmentLoader({
598 hls: this.fakeHls,
599 currentTime() {
600 return currentTime;
601 },
602 mediaSource,
603 seekable: () => this.seekable,
604 withCredentials: false
605 });
606 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
607 loader.mimeType(this.mimeType);
608 loader.load();
609
610 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
611 QUnit.ok(!this.requests[0].withCredentials, 'overrode key request withCredentials');
612 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
613 QUnit.ok(!this.requests[1].withCredentials, 'overrode segment request withCredentials');
614 videojs.options.hls = hlsOptions;
615 });
616
617 QUnit.test('remains ready if there are no segments', function() {
618 loader.playlist(playlistWithDuration(0));
619 loader.mimeType(this.mimeType);
620 loader.load();
621 QUnit.equal(loader.state, 'READY', 'in the ready state');
622 });
623
624 QUnit.test('dispose cleans up outstanding work', function() {
625 loader.playlist(playlistWithDuration(20));
626 loader.mimeType(this.mimeType);
627 loader.load();
628
629 loader.dispose();
630 QUnit.ok(this.requests[0].aborted, 'aborted segment request');
631 QUnit.equal(this.requests.length, 1, 'did not open another request');
632 mediaSource.sourceBuffers.forEach((sourceBuffer, i) => {
633 let lastOperation = sourceBuffer.updates_.slice(-1)[0];
634
635 QUnit.ok(lastOperation.abort, 'aborted source buffer ' + i);
636 });
637 });
638
639 // ----------
640 // Decryption
641 // ----------
642
643 QUnit.test('calling load with an encrypted segment requests key and segment', function() {
644 QUnit.equal(loader.state, 'INIT', 'starts in the init state');
645 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
646 QUnit.equal(loader.state, 'INIT', 'starts in the init state');
647 QUnit.ok(loader.paused(), 'starts paused');
648
649 loader.mimeType(this.mimeType);
650 loader.load();
651 QUnit.equal(loader.state, 'WAITING', 'moves to the ready state');
652 QUnit.ok(!loader.paused(), 'loading is not paused');
653 QUnit.equal(this.requests.length, 2, 'requested a segment and key');
654 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
655 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
656 });
657
658 QUnit.test('cancels outstanding key request on abort', function() {
659 loader.playlist(playlistWithDuration(20, {isEncrypted: true}));
660 loader.mimeType(this.mimeType);
661 loader.load();
662 loader.xhr_.keyXhr.onreadystatechange = function() {
663 throw new Error('onreadystatechange should not be called');
664 };
665
666 QUnit.equal(this.requests.length, 2, 'requested a segment and key');
667 loader.abort();
668 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
669 QUnit.ok(this.requests[0].aborted, 'aborted the first key request');
670 QUnit.equal(this.requests.length, 4, 'started a new request');
671 QUnit.equal(loader.state, 'WAITING', 'back to the waiting state');
672 });
673
674 QUnit.test('dispose cleans up key requests for encrypted segments', function() {
675 loader.playlist(playlistWithDuration(20, {isEncrypted: true}));
676 loader.mimeType(this.mimeType);
677 loader.load();
678
679 loader.dispose();
680 QUnit.equal(this.requests.length, 2, 'requested a segment and key');
681 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
682 QUnit.ok(this.requests[0].aborted, 'aborted the first segment\s key request');
683 QUnit.equal(this.requests.length, 2, 'did not open another request');
684 });
685
686 QUnit.test('key 404s should trigger an error', function() {
687 let errors = [];
688
689 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
690 loader.mimeType(this.mimeType);
691 loader.load();
692 loader.on('error', function(error) {
693 errors.push(error);
694 });
695 this.requests.shift().respond(404, null, '');
696
697 QUnit.equal(errors.length, 1, 'triggered an error');
698 QUnit.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
699 QUnit.equal(loader.error().message, 'HLS key request error at URL: 0-key.php',
700 'receieved a key error message');
701 QUnit.ok(loader.error().xhr, 'included the request object');
702 QUnit.ok(loader.paused(), 'paused the loader');
703 QUnit.equal(loader.state, 'READY', 'returned to the ready state');
704 });
705
706 QUnit.test('key 5xx status codes trigger an error', function() {
707 let errors = [];
708
709 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
710 loader.mimeType(this.mimeType);
711 loader.load();
712 loader.on('error', function(error) {
713 errors.push(error);
714 });
715 this.requests.shift().respond(500, null, '');
716
717 QUnit.equal(errors.length, 1, 'triggered an error');
718 QUnit.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
719 QUnit.equal(loader.error().message, 'HLS key request error at URL: 0-key.php',
720 'receieved a key error message');
721 QUnit.ok(loader.error().xhr, 'included the request object');
722 QUnit.ok(loader.paused(), 'paused the loader');
723 QUnit.equal(loader.state, 'READY', 'returned to the ready state');
724 });
725
726 QUnit.test('the key is saved to the segment in the correct format', function() {
727 let keyRequest;
728 let segmentRequest;
729 let segment;
730 let segmentInfo;
731
732 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
733 loader.mimeType(this.mimeType);
734 loader.load();
735
736 // stop processing so we can examine segment info
737 loader.processResponse_ = function() {};
738
739 keyRequest = this.requests.shift();
740 keyRequest.response = new Uint32Array([0, 1, 2, 3]).buffer;
741 keyRequest.respond(200, null, '');
742
743 segmentRequest = this.requests.shift();
744 segmentRequest.response = new Uint8Array(10).buffer;
745 segmentRequest.respond(200, null, '');
746
747 segmentInfo = loader.pendingSegment_;
748 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
749
750 QUnit.deepEqual(segment.key.bytes,
751 new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
752 'passed the specified segment key');
753 });
754
755 QUnit.test('supplies media sequence of current segment as the IV by default, if no IV ' +
756 'is specified',
757 function() {
758 let keyRequest;
759 let segmentRequest;
760 let segment;
761 let segmentInfo;
762
763 loader.playlist(playlistWithDuration(10, {isEncrypted: true, mediaSequence: 5}));
764 loader.mimeType(this.mimeType);
765 loader.load();
766
767 // stop processing so we can examine segment info
768 loader.processResponse_ = function() {};
769
770 keyRequest = this.requests.shift();
771 keyRequest.response = new Uint32Array([0, 0, 0, 0]).buffer;
772 keyRequest.respond(200, null, '');
773
774 segmentRequest = this.requests.shift();
775 segmentRequest.response = new Uint8Array(10).buffer;
776 segmentRequest.respond(200, null, '');
777
778 segmentInfo = loader.pendingSegment_;
779 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
780
781 QUnit.deepEqual(segment.key.iv, new Uint32Array([0, 0, 0, 5]),
782 'the IV for the segment is the media sequence');
783 });
784
785 QUnit.test('segment with key has decrypted bytes appended during processing', function() {
786 let keyRequest;
787 let segmentRequest;
788
789 // stop processing so we can examine segment info
790 loader.handleSegment_ = function() {};
791
792 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
793 loader.mimeType(this.mimeType);
794 loader.load();
795
796 segmentRequest = this.requests.pop();
797 segmentRequest.response = new Uint8Array(8).buffer;
798 segmentRequest.respond(200, null, '');
799 QUnit.ok(loader.pendingSegment_.encryptedBytes, 'encrypted bytes in segment');
800 QUnit.ok(!loader.pendingSegment_.bytes, 'no decrypted bytes in segment');
801
802 keyRequest = this.requests.shift();
803 keyRequest.response = new Uint32Array([0, 0, 0, 0]).buffer;
804 keyRequest.respond(200, null, '');
805
806 // Allow the decrypter to decrypt
807 this.clock.tick(1);
808 // Allow the decrypter's async stream to run the callback
809 this.clock.tick(1);
810 QUnit.ok(loader.pendingSegment_.bytes, 'decrypted bytes in segment');
811 });
812
813 QUnit.test('calling load with an encrypted segment waits for both key and segment ' +
814 'before processing', function() {
815 let keyRequest;
816 let segmentRequest;
817
818 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
819 loader.mimeType(this.mimeType);
820 loader.load();
821
822 QUnit.equal(loader.state, 'WAITING', 'moves to waiting state');
823 QUnit.equal(this.requests.length, 2, 'requested a segment and key');
824 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
825 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
826 // respond to the segment first
827 segmentRequest = this.requests.pop();
828 segmentRequest.response = new Uint8Array(10).buffer;
829 segmentRequest.respond(200, null, '');
830 QUnit.equal(loader.state, 'WAITING', 'still in waiting state');
831 // then respond to the key
832 keyRequest = this.requests.shift();
833 keyRequest.response = new Uint32Array([0, 0, 0, 0]).buffer;
834 keyRequest.respond(200, null, '');
835 QUnit.equal(loader.state, 'DECRYPTING', 'moves to decrypting state');
836 });
837
838 QUnit.test('key request timeouts reset bandwidth', function() {
839 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
840 loader.mimeType(this.mimeType);
841 loader.load();
842
843 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
844 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
845 // a lot of time passes so the request times out
846 this.requests[0].timedout = true;
847 this.clock.tick(100 * 1000);
848
849 QUnit.equal(loader.bandwidth, 1, 'reset bandwidth');
850 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time');
851 });
852
853 QUnit.module('Segment Loading Calculation', {
854 beforeEach() {
855 this.env = useFakeEnvironment();
856 this.mse = useFakeMediaSource();
857 this.hasPlayed = true;
858
859 currentTime = 0;
860 loader = new SegmentLoader({
861 currentTime() {
862 return currentTime;
863 },
864 mediaSource: new videojs.MediaSource(),
865 hasPlayed: () => this.hasPlayed
866 });
867 },
868 afterEach() {
869 this.env.restore();
870 this.mse.restore();
871 }
872 });
873
874 QUnit.test('requests the first segment with an empty buffer', function() {
875 loader.mimeType(this.mimeType);
876
877 let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges(),
878 playlistWithDuration(20),
879 0);
880
881 QUnit.ok(segmentInfo, 'generated a request');
882 QUnit.equal(segmentInfo.uri, '0.ts', 'requested the first segment');
883 });
884
885 QUnit.test('no request if video not played and 1 segment is buffered', function() {
886 this.hasPlayed = false;
887 loader.mimeType(this.mimeType);
888
889 let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]),
890 playlistWithDuration(20),
891 0);
892
893 QUnit.ok(!segmentInfo, 'no request generated');
894
895 });
896
897 QUnit.test('does not download the next segment if the buffer is full', function() {
898 let buffered;
899 let segmentInfo;
900
901 loader.mimeType(this.mimeType);
902
903 buffered = videojs.createTimeRanges([
904 [0, 15 + GOAL_BUFFER_LENGTH]
905 ]);
906 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(30), 15);
907
908 QUnit.ok(!segmentInfo, 'no segment request generated');
909 });
910
911 QUnit.test('downloads the next segment if the buffer is getting low', function() {
912 let buffered;
913 let segmentInfo;
914
915 loader.mimeType(this.mimeType);
916
917 buffered = videojs.createTimeRanges([[0, 19.999]]);
918 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(30), 15);
919
920 QUnit.ok(segmentInfo, 'made a request');
921 QUnit.equal(segmentInfo.uri, '2.ts', 'requested the third segment');
922 });
923
924 QUnit.test('buffers based on the correct TimeRange if multiple ranges exist', function() {
925 let buffered;
926 let segmentInfo;
927
928 loader.mimeType(this.mimeType);
929
930 buffered = videojs.createTimeRanges([[0, 10], [20, 30]]);
931 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(40), 8);
932
933 QUnit.ok(segmentInfo, 'made a request');
934 QUnit.equal(segmentInfo.uri, '1.ts', 'requested the second segment');
935
936 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(40), 20);
937 QUnit.ok(segmentInfo, 'made a request');
938 QUnit.equal(segmentInfo.uri, '3.ts', 'requested the fourth segment');
939 });
940
941 QUnit.test('stops downloading segments at the end of the playlist', function() {
942 let buffered;
943 let segmentInfo;
944
945 loader.mimeType(this.mimeType);
946
947 buffered = videojs.createTimeRanges([[0, 60]]);
948 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(60), 0);
949
950 QUnit.ok(!segmentInfo, 'no request was made');
951 });
952
953 QUnit.test('stops downloading segments if buffered past reported end of the playlist',
954 function() {
955 let buffered;
956 let segmentInfo;
957 let playlist;
958
959 loader.mimeType(this.mimeType);
960
961 buffered = videojs.createTimeRanges([[0, 59.9]]);
962 playlist = playlistWithDuration(60);
963 playlist.segments[playlist.segments.length - 1].end = 59.9;
964 segmentInfo = loader.checkBuffer_(buffered, playlist, 50);
965
966 QUnit.ok(!segmentInfo, 'no request was made');
967 });
968
969 QUnit.test('calculates timestampOffset for discontinuities', function() {
970 let segmentInfo;
971 let playlist;
972
973 loader.mimeType(this.mimeType);
974
975 playlist = playlistWithDuration(60);
976 playlist.segments[3].end = 37.9;
977 playlist.discontinuityStarts = [4];
978 playlist.segments[4].discontinuity = true;
979 playlist.segments[4].timeline = 1;
980
981 segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 37.9]]), playlist, 36);
982 QUnit.equal(segmentInfo.timestampOffset, 37.9, 'placed the discontinuous segment');
983 });
984
985 QUnit.test('adjusts calculations based on expired time', function() {
986 let buffered;
987 let playlist;
988 let segmentInfo;
989
990 loader.mimeType(this.mimeType);
991
992 buffered = videojs.createTimeRanges([[0, 30]]);
993 playlist = playlistWithDuration(50);
994
995 loader.expired(10);
996
997 segmentInfo = loader.checkBuffer_(buffered,
998 playlist,
999 40 - GOAL_BUFFER_LENGTH);
1000
1001 QUnit.ok(segmentInfo, 'fetched a segment');
1002 QUnit.equal(segmentInfo.uri, '2.ts', 'accounted for expired time');
1003 });
1 import SourceUpdater from '../src/source-updater';
2 import QUnit from 'qunit';
3 import videojs from 'video.js';
4 import { useFakeMediaSource } from './test-helpers';
5
6 QUnit.module('Source Updater', {
7 beforeEach() {
8 this.mse = useFakeMediaSource();
9 this.mediaSource = new videojs.MediaSource();
10 },
11 afterEach() {
12 this.mse.restore();
13 }
14 });
15
16 QUnit.test('waits for sourceopen to create a source buffer', function() {
17 new SourceUpdater(this.mediaSource, 'video/mp2t'); // eslint-disable-line no-new
18
19 QUnit.equal(this.mediaSource.sourceBuffers.length, 0,
20 'waited to create the source buffer');
21
22 this.mediaSource.trigger('sourceopen');
23
24 QUnit.equal(this.mediaSource.sourceBuffers.length, 1, 'created one source buffer');
25 QUnit.equal(this.mediaSource.sourceBuffers[0].mimeType_, 'video/mp2t',
26 'assigned the correct MIME type');
27 });
28
29 QUnit.test('runs a callback when the source buffer is created', function() {
30 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
31 let sourceBuffer;
32
33 updater.appendBuffer(new Uint8Array([0, 1, 2]));
34
35 this.mediaSource.trigger('sourceopen');
36 sourceBuffer = this.mediaSource.sourceBuffers[0];
37 QUnit.equal(sourceBuffer.updates_.length, 1, 'called the source buffer once');
38 QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0, 1, 2]),
39 'appended the bytes');
40 });
41
42 QUnit.test('runs the completion callback when updateend fires', function() {
43 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
44 let updateends = 0;
45 let sourceBuffer;
46
47 this.mediaSource.trigger('sourceopen');
48 sourceBuffer = this.mediaSource.sourceBuffers[0];
49 updater.appendBuffer(new Uint8Array([0, 1, 2]), function() {
50 updateends++;
51 });
52 updater.appendBuffer(new Uint8Array([2, 3, 4]), function() {
53 throw new Error('Wrong completion callback invoked!');
54 });
55
56 QUnit.equal(updateends, 0, 'no completions yet');
57 sourceBuffer.trigger('updateend');
58 QUnit.equal(updateends, 1, 'ran the completion callback');
59 });
60
61 QUnit.test('runs the next callback after updateend fires', function() {
62 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
63 let sourceBuffer;
64
65 updater.appendBuffer(new Uint8Array([0, 1, 2]));
66 this.mediaSource.trigger('sourceopen');
67 sourceBuffer = this.mediaSource.sourceBuffers[0];
68
69 updater.appendBuffer(new Uint8Array([2, 3, 4]));
70 QUnit.equal(sourceBuffer.updates_.length, 1, 'delayed the update');
71
72 sourceBuffer.trigger('updateend');
73 QUnit.equal(sourceBuffer.updates_.length, 2, 'updated twice');
74 QUnit.deepEqual(sourceBuffer.updates_[1].append, new Uint8Array([2, 3, 4]),
75 'appended the bytes');
76 });
77
78 QUnit.test('runs only one callback at a time', function() {
79 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
80 let sourceBuffer;
81
82 updater.appendBuffer(new Uint8Array([0]));
83 updater.appendBuffer(new Uint8Array([1]));
84 this.mediaSource.trigger('sourceopen');
85 sourceBuffer = this.mediaSource.sourceBuffers[0];
86
87 updater.appendBuffer(new Uint8Array([2]));
88 QUnit.equal(sourceBuffer.updates_.length, 1, 'queued some updates');
89 QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0]),
90 'ran the first update');
91
92 sourceBuffer.trigger('updateend');
93 QUnit.equal(sourceBuffer.updates_.length, 2, 'queued some updates');
94 QUnit.deepEqual(sourceBuffer.updates_[1].append, new Uint8Array([1]),
95 'ran the second update');
96
97 updater.appendBuffer(new Uint8Array([3]));
98 sourceBuffer.trigger('updateend');
99 QUnit.equal(sourceBuffer.updates_.length, 3, 'queued the updates');
100 QUnit.deepEqual(sourceBuffer.updates_[2].append, new Uint8Array([2]),
101 'ran the third update');
102
103 sourceBuffer.trigger('updateend');
104 QUnit.equal(sourceBuffer.updates_.length, 4, 'finished the updates');
105 QUnit.deepEqual(sourceBuffer.updates_[3].append, new Uint8Array([3]),
106 'ran the fourth update');
107 });
108
109 QUnit.test('runs updates immediately if possible', function() {
110 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
111 let sourceBuffer;
112
113 this.mediaSource.trigger('sourceopen');
114 sourceBuffer = this.mediaSource.sourceBuffers[0];
115 updater.appendBuffer(new Uint8Array([0]));
116 QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
117 QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0]),
118 'appended the bytes');
119 });
120
121 QUnit.test('supports abort', function() {
122 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
123 let sourceBuffer;
124
125 updater.abort();
126 this.mediaSource.trigger('sourceopen');
127
128 sourceBuffer = this.mediaSource.sourceBuffers[0];
129 QUnit.ok(sourceBuffer.updates_[0].abort, 'aborted the source buffer');
130 });
131
132 QUnit.test('supports buffered', function() {
133 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
134
135 QUnit.equal(updater.buffered().length, 0, 'buffered is empty');
136
137 this.mediaSource.trigger('sourceopen');
138 QUnit.ok(updater.buffered(), 'buffered is defined');
139 });
140
141 QUnit.test('supports removeBuffer', function() {
142 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
143 let sourceBuffer;
144
145 this.mediaSource.trigger('sourceopen');
146 sourceBuffer = this.mediaSource.sourceBuffers[0];
147 updater.remove(1, 14);
148
149 QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
150 QUnit.deepEqual(sourceBuffer.updates_[0].remove, [1, 14], 'removed the time range');
151 });
152
153 QUnit.test('supports setting duration', function() {
154 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
155 let sourceBuffer;
156
157 this.mediaSource.trigger('sourceopen');
158 sourceBuffer = this.mediaSource.sourceBuffers[0];
159 updater.duration(21);
160
161 QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
162 QUnit.deepEqual(sourceBuffer.updates_[0].duration, 21, 'changed duration');
163 });
164
165 QUnit.test('supports timestampOffset', function() {
166 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
167 let sourceBuffer;
168
169 this.mediaSource.trigger('sourceopen');
170 sourceBuffer = this.mediaSource.sourceBuffers[0];
171
172 QUnit.equal(updater.timestampOffset(), 0, 'intialized to zero');
173 updater.timestampOffset(21);
174 QUnit.equal(updater.timestampOffset(), 21, 'reflects changes immediately');
175 QUnit.equal(sourceBuffer.timestampOffset, 21, 'applied the update');
176
177 updater.appendBuffer(new Uint8Array(2));
178 updater.timestampOffset(14);
179 QUnit.equal(updater.timestampOffset(), 14, 'reflects changes immediately');
180 QUnit.equal(sourceBuffer.timestampOffset, 21, 'queues application after updates');
181
182 sourceBuffer.trigger('updateend');
183 QUnit.equal(sourceBuffer.timestampOffset, 14, 'applied the update');
184 });
1 import document from 'global/document';
2 import sinon from 'sinon';
3 import videojs from 'video.js';
4 import QUnit from 'qunit';
5 /* eslint-disable no-unused-vars */
6 // needed so MediaSource can be registered with videojs
7 import MediaSource from 'videojs-contrib-media-sources';
8 /* eslint-enable */
9 import testDataManifests from './test-manifests.js';
10 import xhrFactory from '../src/xhr';
11
12 // a SourceBuffer that tracks updates but otherwise is a noop
13 class MockSourceBuffer extends videojs.EventTarget {
14 constructor() {
15 super();
16 this.updates_ = [];
17
18 this.updating = false;
19 this.on('updateend', function() {
20 this.updating = false;
21 });
22
23 this.buffered = videojs.createTimeRanges();
24 this.duration_ = NaN;
25
26 Object.defineProperty(this, 'duration', {
27 get() {
28 return this.duration_;
29 },
30 set(duration) {
31 this.updates_.push({
32 duration
33 });
34 this.duration_ = duration;
35 }
36 });
37 }
38
39 abort() {
40 this.updates_.push({
41 abort: true
42 });
43 }
44
45 appendBuffer(bytes) {
46 this.updates_.push({
47 append: bytes
48 });
49 this.updating = true;
50 }
51
52 remove(start, end) {
53 this.updates_.push({
54 remove: [start, end]
55 });
56 }
57 }
58
59 class MockMediaSource extends videojs.EventTarget {
60 constructor() {
61 super();
62 this.readyState = 'closed';
63 this.on('sourceopen', function() {
64 this.readyState = 'open';
65 });
66
67 this.sourceBuffers = [];
68 this.duration = NaN;
69 this.seekable = videojs.createTimeRange();
70 }
71
72 addSeekableRange_(start, end) {
73 this.seekable = videojs.createTimeRange(start, end);
74 }
75
76 addSourceBuffer(mime) {
77 let sourceBuffer = new MockSourceBuffer();
78
79 sourceBuffer.mimeType_ = mime;
80 this.sourceBuffers.push(sourceBuffer);
81 return sourceBuffer;
82 }
83
84 endOfStream(error) {
85 this.readyState = 'closed';
86 this.error_ = error;
87 }
88 }
89
90 export const useFakeMediaSource = function() {
91 let RealMediaSource = videojs.MediaSource;
92 let realCreateObjectURL = window.URL.createObjectURL;
93 let id = 0;
94
95 videojs.MediaSource = MockMediaSource;
96 videojs.MediaSource.supportsNativeMediaSources =
97 RealMediaSource.supportsNativeMediaSources;
98 videojs.URL.createObjectURL = function() {
99 id++;
100 return 'blob:videojs-contrib-hls-mock-url' + id;
101 };
102
103 return {
104 restore() {
105 videojs.MediaSource = RealMediaSource;
106 videojs.URL.createObjectURL = realCreateObjectURL;
107 }
108 };
109 };
110
111 let fakeEnvironment = {
112 requests: [],
113 restore() {
114 this.clock.restore();
115 videojs.xhr.XMLHttpRequest = window.XMLHttpRequest;
116 this.xhr.restore();
117 ['warn', 'error'].forEach((level) => {
118 if (this.log && this.log[level] && this.log[level].restore) {
119 QUnit.equal(this.log[level].callCount, 0, `no unexpected logs on ${level}`);
120 this.log[level].restore();
121 }
122 });
123 }
124 };
125
126 export const useFakeEnvironment = function() {
127 fakeEnvironment.log = {};
128 ['warn', 'error'].forEach((level) => {
129 // you can use .log[level].args to get args
130 sinon.stub(videojs.log, level);
131 fakeEnvironment.log[level] = videojs.log[level];
132 Object.defineProperty(videojs.log[level], 'calls', {
133 get() {
134 // reset callCount to 0 so they don't have to
135 let callCount = this.callCount;
136
137 this.callCount = 0;
138 return callCount;
139 }
140 });
141 });
142 fakeEnvironment.clock = sinon.useFakeTimers();
143 fakeEnvironment.xhr = sinon.useFakeXMLHttpRequest();
144 fakeEnvironment.requests.length = 0;
145 fakeEnvironment.xhr.onCreate = function(xhr) {
146 fakeEnvironment.requests.push(xhr);
147 };
148 videojs.xhr.XMLHttpRequest = fakeEnvironment.xhr;
149
150 return fakeEnvironment;
151 };
152
153 // patch over some methods of the provided tech so it can be tested
154 // synchronously with sinon's fake timers
155 export const mockTech = function(tech) {
156 if (tech.isMocked_) {
157 // make this function idempotent because HTML and Flash based
158 // playback have very different lifecycles. For HTML, the tech
159 // is available on player creation. For Flash, the tech isn't
160 // ready until the source has been loaded and one tick has
161 // expired.
162 return;
163 }
164
165 tech.isMocked_ = true;
166 tech.src_ = null;
167 tech.time_ = null;
168
169 tech.paused_ = !tech.autoplay();
170 tech.paused = function() {
171 return tech.paused_;
172 };
173
174 if (!tech.currentTime_) {
175 tech.currentTime_ = tech.currentTime;
176 }
177 tech.currentTime = function() {
178 return tech.time_ === null ? tech.currentTime_() : tech.time_;
179 };
180
181 tech.setSrc = function(src) {
182 tech.src_ = src;
183 };
184 tech.src = function(src) {
185 if (src !== null) {
186 return tech.setSrc(src);
187 }
188 return tech.src_ === null ? tech.src : tech.src_;
189 };
190 tech.currentSrc_ = tech.currentSrc;
191 tech.currentSrc = function() {
192 return tech.src_ === null ? tech.currentSrc_() : tech.src_;
193 };
194
195 tech.play_ = tech.play;
196 tech.play = function() {
197 tech.play_();
198 tech.paused_ = false;
199 tech.trigger('play');
200 };
201 tech.pause_ = tech.pause_;
202 tech.pause = function() {
203 tech.pause_();
204 tech.paused_ = true;
205 tech.trigger('pause');
206 };
207
208 tech.setCurrentTime = function(time) {
209 tech.time_ = time;
210
211 setTimeout(function() {
212 tech.trigger('seeking');
213 setTimeout(function() {
214 tech.trigger('seeked');
215 }, 1);
216 }, 1);
217 };
218 };
219
220 export const createPlayer = function(options) {
221 let video;
222 let player;
223
224 video = document.createElement('video');
225 video.className = 'video-js';
226 document.querySelector('#qunit-fixture').appendChild(video);
227 player = videojs(video, options || {
228 flash: {
229 swf: ''
230 }
231 });
232
233 player.buffered = function() {
234 return videojs.createTimeRange(0, 0);
235 };
236 mockTech(player.tech_);
237
238 return player;
239 };
240
241 export const openMediaSource = function(player, clock) {
242 // ensure the Flash tech is ready
243 player.tech_.triggerReady();
244 clock.tick(1);
245 // mock the tech *after* it has finished loading so that we don't
246 // mock a tech that will be unloaded on the next tick
247 mockTech(player.tech_);
248 player.tech_.hls.xhr = xhrFactory();
249
250 // simulate the sourceopen event
251 player.tech_.hls.mediaSource.readyState = 'open';
252 player.tech_.hls.mediaSource.dispatchEvent({
253 type: 'sourceopen',
254 swfId: player.tech_.el().id
255 });
256 };
257
258 export const standardXHRResponse = function(request) {
259 if (!request.url) {
260 return;
261 }
262
263 let contentType = 'application/json';
264 // contents off the global object
265 let manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
266
267 if (manifestName) {
268 manifestName = manifestName[1];
269 } else {
270 manifestName = request.url;
271 }
272
273 if (/\.m3u8?/.test(request.url)) {
274 contentType = 'application/vnd.apple.mpegurl';
275 } else if (/\.ts/.test(request.url)) {
276 contentType = 'video/MP2T';
277 }
278
279 request.response = new Uint8Array(16).buffer;
280 request.respond(200, { 'Content-Type': contentType },
281 testDataManifests[manifestName]);
282 };
283
284 // return an absolute version of a page-relative URL
285 export const absoluteUrl = function(relativeUrl) {
286 return window.location.protocol + '//' +
287 window.location.host +
288 (window.location.pathname
289 .split('/')
290 .slice(0, -1)
291 .concat(relativeUrl)
292 .join('/')
293 );
294 };
This diff could not be displayed because it is too large.
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "http://example.com/00001.ts" 9 "uri": "http://example.com/00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "https://example.com/00002.ts" 14 "uri": "https://example.com/00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "//example.com/00003.ts" 19 "uri": "//example.com/00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "http://example.com/00004.ts" 24 "uri": "http://example.com/00004.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 10, 29 "duration": 10,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 10, 38 "duration": 10,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,6 +153,7 @@ ...@@ -137,6 +153,7 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 1.4167, 155 "duration": 1.4167,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
...@@ -144,4 +161,4 @@ ...@@ -144,4 +161,4 @@
144 "endList": true, 161 "endList": true,
145 "discontinuitySequence": 0, 162 "discontinuitySequence": 0,
146 "discontinuityStarts": [] 163 "discontinuityStarts": []
147 }
...\ No newline at end of file ...\ No newline at end of file
164 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
...@@ -16,4 +17,4 @@ ...@@ -16,4 +17,4 @@
16 "endList": true, 17 "endList": true,
17 "discontinuitySequence": 0, 18 "discontinuitySequence": 0,
18 "discontinuityStarts": [] 19 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
20 }
......
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 // TYPE
6 AUDIO: {
7 // GROUP-ID
8 "audio": {
9 // NAME
10 "English": {
11 language: 'eng',
12 autoselect: true,
13 default: true,
14 uri: "eng/prog_index.m3u8"
15 },
16 // NAME
17 "Français": {
18 language: "fre",
19 autoselect: true,
20 default: false,
21 uri: "fre/prog_index.m3u8"
22 },
23 // NAME
24 "Espanol": {
25 language: "sp",
26 autoselect: true,
27 default: false,
28 uri: "sp/prog_index.m3u8"
29 }
30 }
31 },
32 VIDEO: {},
33 "CLOSED-CAPTIONS": {},
34 SUBTITLES: {}
35 },
36 playlists: [{
37 attributes: {
38 "PROGRAM-ID": 1,
39 BANDWIDTH: 195023,
40 CODECS: "avc1.42e00a,mp4a.40.2",
41 AUDIO: 'audio'
42 },
43 timeline: 0,
44 uri: "lo/prog_index.m3u8"
45 }, {
46 attributes: {
47 "PROGRAM-ID": 1,
48 BANDWIDTH: 591680,
49 CODECS: "avc1.42e01e,mp4a.40.2",
50 AUDIO: 'audio'
51 },
52 timeline: 0,
53 uri: "hi/prog_index.m3u8"
54 }]
55 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
3 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
5
6 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="avc1.42e00a,mp4a.40.2",AUDIO="audio"
7 lo/prog_index.m3u8
8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio"
9 hi/prog_index.m3u8
...\ No newline at end of file ...\ No newline at end of file
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 aac: {
7 English: {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 uri: "eng/prog_index.m3u8"
12 }
13 }
14 },
15 VIDEO: {
16 "500kbs": {
17 Angle1: {
18 autoselect: true,
19 default: true
20 },
21 Angle2: {
22 autoselect: true,
23 default: false,
24 uri: "Angle2/500kbs/prog_index.m3u8"
25 },
26 Angle3: {
27 autoselect: true,
28 default: false,
29 uri: "Angle3/500kbs/prog_index.m3u8"
30 }
31 }
32 },
33 "CLOSED-CAPTIONS": {},
34 SUBTITLES: {}
35 },
36 playlists: [{
37 attributes: {
38 "PROGRAM-ID": 1,
39 BANDWIDTH: 754857,
40 CODECS: "mp4a.40.2,avc1.4d401e",
41 AUDIO: "aac",
42 VIDEO: "500kbs"
43 },
44 timeline: 0,
45 uri: "Angle1/500kbs/prog_index.m3u8"
46 }]
47 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
3 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8"
7 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac"
8 Angle1/500kbs/prog_index.m3u8
...\ No newline at end of file ...\ No newline at end of file
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
10 "height": 224 10 "height": 224
11 } 11 }
12 }, 12 },
13 "timeline": 0,
13 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" 14 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
14 }, 15 },
15 { 16 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "PROGRAM-ID": 1, 18 "PROGRAM-ID": 1,
18 "BANDWIDTH": 40000 19 "BANDWIDTH": 40000
19 }, 20 },
21 "timeline": 0,
20 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" 22 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
21 }, 23 },
22 { 24 {
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
28 "height": 224 30 "height": 224
29 } 31 }
30 }, 32 },
33 "timeline": 0,
31 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" 34 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
32 }, 35 },
33 { 36 {
...@@ -39,8 +42,15 @@ ...@@ -39,8 +42,15 @@
39 "height": 540 42 "height": 540
40 } 43 }
41 }, 44 },
45 "timeline": 0,
42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 46 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
43 } 47 }
44 ], 48 ],
45 "discontinuityStarts": []
46 }
...\ No newline at end of file ...\ No newline at end of file
49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
56 }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "hls_450k_video.ts" 9 "uri": "hls_450k_video.ts"
9 }, 10 },
10 { 11 {
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
13 "offset": 522828 14 "offset": 522828
14 }, 15 },
15 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
16 "uri": "hls_450k_video.ts" 18 "uri": "hls_450k_video.ts"
17 }, 19 },
18 { 20 {
...@@ -21,6 +23,7 @@ ...@@ -21,6 +23,7 @@
21 "offset": 0 23 "offset": 0
22 }, 24 },
23 "duration": 10, 25 "duration": 10,
26 "timeline": 0,
24 "uri": "hls_450k_video2.ts" 27 "uri": "hls_450k_video2.ts"
25 }, 28 },
26 { 29 {
...@@ -29,6 +32,7 @@ ...@@ -29,6 +32,7 @@
29 "offset": 1823412 32 "offset": 1823412
30 }, 33 },
31 "duration": 10, 34 "duration": 10,
35 "timeline": 0,
32 "uri": "hls_450k_video.ts" 36 "uri": "hls_450k_video.ts"
33 }, 37 },
34 { 38 {
...@@ -37,6 +41,7 @@ ...@@ -37,6 +41,7 @@
37 "offset": 2299992 41 "offset": 2299992
38 }, 42 },
39 "duration": 10, 43 "duration": 10,
44 "timeline": 0,
40 "uri": "hls_450k_video.ts" 45 "uri": "hls_450k_video.ts"
41 }, 46 },
42 { 47 {
...@@ -45,6 +50,7 @@ ...@@ -45,6 +50,7 @@
45 "offset": 2835604 50 "offset": 2835604
46 }, 51 },
47 "duration": 10, 52 "duration": 10,
53 "timeline": 0,
48 "uri": "hls_450k_video.ts" 54 "uri": "hls_450k_video.ts"
49 }, 55 },
50 { 56 {
...@@ -53,6 +59,7 @@ ...@@ -53,6 +59,7 @@
53 "offset": 3042780 59 "offset": 3042780
54 }, 60 },
55 "duration": 10, 61 "duration": 10,
62 "timeline": 0,
56 "uri": "hls_450k_video.ts" 63 "uri": "hls_450k_video.ts"
57 }, 64 },
58 { 65 {
...@@ -61,6 +68,7 @@ ...@@ -61,6 +68,7 @@
61 "offset": 3498680 68 "offset": 3498680
62 }, 69 },
63 "duration": 10, 70 "duration": 10,
71 "timeline": 0,
64 "uri": "hls_450k_video.ts" 72 "uri": "hls_450k_video.ts"
65 }, 73 },
66 { 74 {
...@@ -69,6 +77,7 @@ ...@@ -69,6 +77,7 @@
69 "offset": 4155928 77 "offset": 4155928
70 }, 78 },
71 "duration": 10, 79 "duration": 10,
80 "timeline": 0,
72 "uri": "hls_450k_video.ts" 81 "uri": "hls_450k_video.ts"
73 }, 82 },
74 { 83 {
...@@ -77,6 +86,7 @@ ...@@ -77,6 +86,7 @@
77 "offset": 4727636 86 "offset": 4727636
78 }, 87 },
79 "duration": 10, 88 "duration": 10,
89 "timeline": 0,
80 "uri": "hls_450k_video.ts" 90 "uri": "hls_450k_video.ts"
81 }, 91 },
82 { 92 {
...@@ -85,6 +95,7 @@ ...@@ -85,6 +95,7 @@
85 "offset": 5212676 95 "offset": 5212676
86 }, 96 },
87 "duration": 10, 97 "duration": 10,
98 "timeline": 0,
88 "uri": "hls_450k_video.ts" 99 "uri": "hls_450k_video.ts"
89 }, 100 },
90 { 101 {
...@@ -93,6 +104,7 @@ ...@@ -93,6 +104,7 @@
93 "offset": 5921812 104 "offset": 5921812
94 }, 105 },
95 "duration": 10, 106 "duration": 10,
107 "timeline": 0,
96 "uri": "hls_450k_video.ts" 108 "uri": "hls_450k_video.ts"
97 }, 109 },
98 { 110 {
...@@ -101,6 +113,7 @@ ...@@ -101,6 +113,7 @@
101 "offset": 6651816 113 "offset": 6651816
102 }, 114 },
103 "duration": 10, 115 "duration": 10,
116 "timeline": 0,
104 "uri": "hls_450k_video.ts" 117 "uri": "hls_450k_video.ts"
105 }, 118 },
106 { 119 {
...@@ -109,6 +122,7 @@ ...@@ -109,6 +122,7 @@
109 "offset": 7108092 122 "offset": 7108092
110 }, 123 },
111 "duration": 10, 124 "duration": 10,
125 "timeline": 0,
112 "uri": "hls_450k_video.ts" 126 "uri": "hls_450k_video.ts"
113 }, 127 },
114 { 128 {
...@@ -117,6 +131,7 @@ ...@@ -117,6 +131,7 @@
117 "offset": 7576776 131 "offset": 7576776
118 }, 132 },
119 "duration": 10, 133 "duration": 10,
134 "timeline": 0,
120 "uri": "hls_450k_video.ts" 135 "uri": "hls_450k_video.ts"
121 }, 136 },
122 { 137 {
...@@ -125,6 +140,7 @@ ...@@ -125,6 +140,7 @@
125 "offset": 8021772 140 "offset": 8021772
126 }, 141 },
127 "duration": 10, 142 "duration": 10,
143 "timeline": 0,
128 "uri": "hls_450k_video.ts" 144 "uri": "hls_450k_video.ts"
129 }, 145 },
130 { 146 {
...@@ -133,6 +149,7 @@ ...@@ -133,6 +149,7 @@
133 "offset": 8353216 149 "offset": 8353216
134 }, 150 },
135 "duration": 1.4167, 151 "duration": 1.4167,
152 "timeline": 0,
136 "uri": "hls_450k_video.ts" 153 "uri": "hls_450k_video.ts"
137 } 154 }
138 ], 155 ],
...@@ -140,4 +157,4 @@ ...@@ -140,4 +157,4 @@
140 "endList": true, 157 "endList": true,
141 "discontinuitySequence": 0, 158 "discontinuitySequence": 0,
142 "discontinuityStarts": [] 159 "discontinuityStarts": []
143 }
...\ No newline at end of file ...\ No newline at end of file
160 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
...@@ -16,4 +17,4 @@ ...@@ -16,4 +17,4 @@
16 "endList": true, 17 "endList": true,
17 "discontinuitySequence": 0, 18 "discontinuitySequence": 0,
18 "discontinuityStarts": [] 19 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
20 }
......
...@@ -5,19 +5,23 @@ ...@@ -5,19 +5,23 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 3,
8 "uri": "001.ts" 9 "uri": "001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 19, 12 "duration": 19,
13 "timeline": 3,
12 "uri": "002.ts" 14 "uri": "002.ts"
13 }, 15 },
14 { 16 {
15 "discontinuity": true, 17 "discontinuity": true,
16 "duration": 10, 18 "duration": 10,
19 "timeline": 4,
17 "uri": "003.ts" 20 "uri": "003.ts"
18 }, 21 },
19 { 22 {
20 "duration": 11, 23 "duration": 11,
24 "timeline": 4,
21 "uri": "004.ts" 25 "uri": "004.ts"
22 } 26 }
23 ], 27 ],
......
...@@ -5,41 +5,50 @@ ...@@ -5,41 +5,50 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "001.ts" 9 "uri": "001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 19, 12 "duration": 19,
13 "timeline": 0,
12 "uri": "002.ts" 14 "uri": "002.ts"
13 }, 15 },
14 { 16 {
15 "discontinuity": true, 17 "discontinuity": true,
16 "duration": 10, 18 "duration": 10,
19 "timeline": 1,
17 "uri": "003.ts" 20 "uri": "003.ts"
18 }, 21 },
19 { 22 {
20 "duration": 11, 23 "duration": 11,
24 "timeline": 1,
21 "uri": "004.ts" 25 "uri": "004.ts"
22 }, 26 },
23 { 27 {
24 "discontinuity": true, 28 "discontinuity": true,
25 "duration": 10, 29 "duration": 10,
30 "timeline": 2,
26 "uri": "005.ts" 31 "uri": "005.ts"
27 }, 32 },
28 { 33 {
29 "duration": 10, 34 "duration": 10,
35 "timeline": 2,
30 "uri": "006.ts" 36 "uri": "006.ts"
31 }, 37 },
32 { 38 {
33 "duration": 10, 39 "duration": 10,
40 "timeline": 2,
34 "uri": "007.ts" 41 "uri": "007.ts"
35 }, 42 },
36 { 43 {
37 "discontinuity": true, 44 "discontinuity": true,
38 "duration": 10, 45 "duration": 10,
46 "timeline": 3,
39 "uri": "008.ts" 47 "uri": "008.ts"
40 }, 48 },
41 { 49 {
42 "duration": 16, 50 "duration": 16,
51 "timeline": 3,
43 "uri": "009.ts" 52 "uri": "009.ts"
44 } 53 }
45 ], 54 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "/00001.ts" 9 "uri": "/00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "/subdir/00002.ts" 14 "uri": "/subdir/00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "/00003.ts" 19 "uri": "/00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "/00004.ts" 24 "uri": "/00004.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
...@@ -16,4 +17,4 @@ ...@@ -16,4 +17,4 @@
16 "endList": true, 17 "endList": true,
17 "discontinuitySequence": 0, 18 "discontinuitySequence": 0,
18 "discontinuityStarts": [] 19 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
20 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -4,26 +4,32 @@ ...@@ -4,26 +4,32 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts" 8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts" 13 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts" 18 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 10, 21 "duration": 10,
22 "timeline": 0,
19 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts" 23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts" 28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
24 }, 29 },
25 { 30 {
26 "duration": 8, 31 "duration": 8,
32 "timeline": 0,
27 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 33 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
28 } 34 }
29 ], 35 ],
...@@ -31,4 +37,4 @@ ...@@ -31,4 +37,4 @@
31 "endList": true, 37 "endList": true,
32 "discontinuitySequence": 0, 38 "discontinuitySequence": 0,
33 "discontinuityStarts": [] 39 "discontinuityStarts": []
34 }
...\ No newline at end of file ...\ No newline at end of file
40 }
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
10 "height": 224 10 "height": 224
11 } 11 }
12 }, 12 },
13 "timeline": 0,
13 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" 14 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
14 }, 15 },
15 { 16 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "PROGRAM-ID": 1, 18 "PROGRAM-ID": 1,
18 "BANDWIDTH": 40000 19 "BANDWIDTH": 40000
19 }, 20 },
21 "timeline": 0,
20 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" 22 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
21 }, 23 },
22 { 24 {
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
28 "height": 224 30 "height": 224
29 } 31 }
30 }, 32 },
33 "timeline": 0,
31 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" 34 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
32 }, 35 },
33 { 36 {
...@@ -39,8 +42,15 @@ ...@@ -39,8 +42,15 @@
39 "height": 540 42 "height": 540
40 } 43 }
41 }, 44 },
45 "timeline": 0,
42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 46 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
43 } 47 }
44 ], 48 ],
45 "discontinuityStarts": []
46 }
...\ No newline at end of file ...\ No newline at end of file
49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
56 }
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
6 "segments": [ 6 "segments": [
7 { 7 {
8 "duration": 2.833, 8 "duration": 2.833,
9 "timeline": 0,
9 "key": { 10 "key": {
10 "method": "AES-128", 11 "method": "AES-128",
11 "uri": "https://priv.example.com/key.php?r=52" 12 "uri": "https://priv.example.com/key.php?r=52"
...@@ -14,6 +15,7 @@ ...@@ -14,6 +15,7 @@
14 }, 15 },
15 { 16 {
16 "duration": 15, 17 "duration": 15,
18 "timeline": 0,
17 "key": { 19 "key": {
18 "method": "AES-128", 20 "method": "AES-128",
19 "uri": "https://priv.example.com/key.php?r=52" 21 "uri": "https://priv.example.com/key.php?r=52"
...@@ -22,6 +24,7 @@ ...@@ -22,6 +24,7 @@
22 }, 24 },
23 { 25 {
24 "duration": 13.333, 26 "duration": 13.333,
27 "timeline": 0,
25 "key": { 28 "key": {
26 "method": "AES-128", 29 "method": "AES-128",
27 "uri": "https://priv.example.com/key.php?r=52" 30 "uri": "https://priv.example.com/key.php?r=52"
...@@ -30,6 +33,7 @@ ...@@ -30,6 +33,7 @@
30 }, 33 },
31 { 34 {
32 "duration": 15, 35 "duration": 15,
36 "timeline": 0,
33 "key": { 37 "key": {
34 "method": "AES-128", 38 "method": "AES-128",
35 "uri": "https://priv.example.com/key.php?r=53" 39 "uri": "https://priv.example.com/key.php?r=53"
...@@ -38,6 +42,7 @@ ...@@ -38,6 +42,7 @@
38 }, 42 },
39 { 43 {
40 "duration": 14, 44 "duration": 14,
45 "timeline": 0,
41 "key": { 46 "key": {
42 "method": "AES-128", 47 "method": "AES-128",
43 "uri": "https://priv.example.com/key.php?r=54", 48 "uri": "https://priv.example.com/key.php?r=54",
...@@ -47,6 +52,7 @@ ...@@ -47,6 +52,7 @@
47 }, 52 },
48 { 53 {
49 "duration": 15, 54 "duration": 15,
55 "timeline": 0,
50 "uri": "http://media.example.com/fileSequence53-B.ts" 56 "uri": "http://media.example.com/fileSequence53-B.ts"
51 } 57 }
52 ], 58 ],
......
...@@ -5,26 +5,32 @@ ...@@ -5,26 +5,32 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts" 9 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts" 14 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts" 19 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts" 24 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
21 }, 25 },
22 { 26 {
23 "duration": 10, 27 "duration": 10,
28 "timeline": 0,
24 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts" 29 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
25 }, 30 },
26 { 31 {
27 "duration": 8, 32 "duration": 8,
33 "timeline": 0,
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 34 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 } 35 }
30 ], 36 ],
...@@ -32,4 +38,4 @@ ...@@ -32,4 +38,4 @@
32 "endList": true, 38 "endList": true,
33 "discontinuitySequence": 0, 39 "discontinuitySequence": 0,
34 "discontinuityStarts": [] 40 "discontinuityStarts": []
35 }
...\ No newline at end of file ...\ No newline at end of file
41 }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 6.64, 6 "duration": 6.64,
7 "timeline": 0,
7 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
8 } 9 }
9 ], 10 ],
...@@ -11,4 +12,4 @@ ...@@ -11,4 +12,4 @@
11 "endList": true, 12 "endList": true,
12 "discontinuitySequence": 0, 13 "discontinuitySequence": 0,
13 "discontinuityStarts": [] 14 "discontinuityStarts": []
14 }
...\ No newline at end of file ...\ No newline at end of file
15 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 5, 29 "duration": 5,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 9.7, 38 "duration": 9.7,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,6 +153,7 @@ ...@@ -137,6 +153,7 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 10, 155 "duration": 10,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
...@@ -144,4 +161,4 @@ ...@@ -144,4 +161,4 @@
144 "endList": true, 161 "endList": true,
145 "discontinuitySequence": 0, 162 "discontinuitySequence": 0,
146 "discontinuityStarts": [] 163 "discontinuityStarts": []
147 }
...\ No newline at end of file ...\ No newline at end of file
164 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
...@@ -16,4 +17,4 @@ ...@@ -16,4 +17,4 @@
16 "endList": true, 17 "endList": true,
17 "discontinuitySequence": 0, 18 "discontinuitySequence": 0,
18 "discontinuityStarts": [] 19 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
20 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -4,26 +4,32 @@ ...@@ -4,26 +4,32 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts" 8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts" 13 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts" 18 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 10, 21 "duration": 10,
22 "timeline": 0,
19 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts" 23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts" 28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
24 }, 29 },
25 { 30 {
26 "duration": 8, 31 "duration": 8,
32 "timeline": 0,
27 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 33 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
28 } 34 }
29 ], 35 ],
...@@ -31,4 +37,4 @@ ...@@ -31,4 +37,4 @@
31 "endList": true, 37 "endList": true,
32 "discontinuitySequence": 0, 38 "discontinuitySequence": 0,
33 "discontinuityStarts": [] 39 "discontinuityStarts": []
34 }
...\ No newline at end of file ...\ No newline at end of file
40 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 10, 29 "duration": 10,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 10, 38 "duration": 10,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,10 +153,11 @@ ...@@ -137,10 +153,11 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 1.4167, 155 "duration": 1.4167,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
143 "endList": true, 160 "endList": true,
144 "discontinuitySequence": 0, 161 "discontinuitySequence": 0,
145 "discontinuityStarts": [] 162 "discontinuityStarts": []
146 }
...\ No newline at end of file ...\ No newline at end of file
163 }
......
...@@ -5,18 +5,21 @@ ...@@ -5,18 +5,21 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 8, 12 "duration": 8,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 8, 17 "duration": 8,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 } 20 }
18 ], 21 ],
19 "targetDuration": 8, 22 "targetDuration": 8,
20 "discontinuitySequence": 0, 23 "discontinuitySequence": 0,
21 "discontinuityStarts": [] 24 "discontinuityStarts": []
22 }
...\ No newline at end of file ...\ No newline at end of file
25 }
......
...@@ -4,42 +4,51 @@ ...@@ -4,42 +4,51 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "001.ts" 8 "uri": "001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 19, 11 "duration": 19,
12 "timeline": 0,
11 "uri": "002.ts" 13 "uri": "002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "003.ts" 18 "uri": "003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 11, 21 "duration": 11,
22 "timeline": 0,
19 "uri": "004.ts" 23 "uri": "004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "005.ts" 28 "uri": "005.ts"
24 }, 29 },
25 { 30 {
26 "duration": 10, 31 "duration": 10,
32 "timeline": 0,
27 "uri": "006.ts" 33 "uri": "006.ts"
28 }, 34 },
29 { 35 {
30 "duration": 10, 36 "duration": 10,
37 "timeline": 0,
31 "uri": "007.ts" 38 "uri": "007.ts"
32 }, 39 },
33 { 40 {
34 "duration": 10, 41 "duration": 10,
42 "timeline": 0,
35 "uri": "008.ts" 43 "uri": "008.ts"
36 }, 44 },
37 { 45 {
38 "duration": 16, 46 "duration": 16,
47 "timeline": 0,
39 "uri": "009.ts" 48 "uri": "009.ts"
40 } 49 }
41 ], 50 ],
42 "targetDuration": 10, 51 "targetDuration": 10,
43 "discontinuitySequence": 0, 52 "discontinuitySequence": 0,
44 "discontinuityStarts": [] 53 "discontinuityStarts": []
45 }
...\ No newline at end of file ...\ No newline at end of file
54 }
......
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
8 } 9 }
9 ], 10 ],
10 "endList": true, 11 "endList": true,
11 "discontinuitySequence": 0, 12 "discontinuitySequence": 0,
12 "discontinuityStarts": [] 13 "discontinuityStarts": []
13 }
...\ No newline at end of file ...\ No newline at end of file
14 }
......
...@@ -4,22 +4,27 @@ ...@@ -4,22 +4,27 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "/test/ts-files/zencoder/gogo/00002.ts" 13 "uri": "/test/ts-files/zencoder/gogo/00002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "/test/ts-files/zencoder/gogo/00003.ts" 18 "uri": "/test/ts-files/zencoder/gogo/00003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 10, 21 "duration": 10,
22 "timeline": 0,
19 "uri": "/test/ts-files/zencoder/gogo/00004.ts" 23 "uri": "/test/ts-files/zencoder/gogo/00004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "/test/ts-files/zencoder/gogo/00005.ts" 28 "uri": "/test/ts-files/zencoder/gogo/00005.ts"
24 } 29 }
25 ], 30 ],
...@@ -27,4 +32,4 @@ ...@@ -27,4 +32,4 @@
27 "endList": true, 32 "endList": true,
28 "discontinuitySequence": 0, 33 "discontinuitySequence": 0,
29 "discontinuityStarts": [] 34 "discontinuityStarts": []
30 }
...\ No newline at end of file ...\ No newline at end of file
35 }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
8 } 9 }
9 ], 10 ],
...@@ -11,4 +12,4 @@ ...@@ -11,4 +12,4 @@
11 "endList": true, 12 "endList": true,
12 "discontinuitySequence": 0, 13 "discontinuitySequence": 0,
13 "discontinuityStarts": [] 14 "discontinuityStarts": []
14 }
...\ No newline at end of file ...\ No newline at end of file
15 }
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
10 "height": 224 10 "height": 224
11 } 11 }
12 }, 12 },
13 "timeline": 0,
13 "uri": "media.m3u8" 14 "uri": "media.m3u8"
14 }, 15 },
15 { 16 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "PROGRAM-ID": 1, 18 "PROGRAM-ID": 1,
18 "BANDWIDTH": 40000 19 "BANDWIDTH": 40000
19 }, 20 },
21 "timeline": 0,
20 "uri": "media1.m3u8" 22 "uri": "media1.m3u8"
21 }, 23 },
22 { 24 {
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
28 "height": 224 30 "height": 224
29 } 31 }
30 }, 32 },
33 "timeline": 0,
31 "uri": "media2.m3u8" 34 "uri": "media2.m3u8"
32 }, 35 },
33 { 36 {
...@@ -39,8 +42,15 @@ ...@@ -39,8 +42,15 @@
39 "height": 540 42 "height": 540
40 } 43 }
41 }, 44 },
45 "timeline": 0,
42 "uri": "media3.m3u8" 46 "uri": "media3.m3u8"
43 } 47 }
44 ], 48 ],
45 "discontinuityStarts": []
46 }
...\ No newline at end of file ...\ No newline at end of file
49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
56 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "media-00001.ts" 9 "uri": "media-00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "media-00002.ts" 14 "uri": "media-00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "media-00003.ts" 19 "uri": "media-00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "media-00004.ts" 24 "uri": "media-00004.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -4,14 +4,16 @@ ...@@ -4,14 +4,16 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "00001.ts" 8 "uri": "00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "00002.ts" 13 "uri": "00002.ts"
12 } 14 }
13 ], 15 ],
14 "targetDuration": 10, 16 "targetDuration": 10,
15 "discontinuitySequence": 0, 17 "discontinuitySequence": 0,
16 "discontinuityStarts": [] 18 "discontinuityStarts": []
17 }
...\ No newline at end of file ...\ No newline at end of file
19 }
......
...@@ -5,14 +5,17 @@ ...@@ -5,14 +5,17 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "hls_450k_video.ts" 9 "uri": "hls_450k_video.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "hls_450k_video.ts" 14 "uri": "hls_450k_video.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "hls_450k_video.ts" 19 "uri": "hls_450k_video.ts"
17 } 20 }
18 ], 21 ],
...@@ -20,4 +23,4 @@ ...@@ -20,4 +23,4 @@
20 "endList": true, 23 "endList": true,
21 "discontinuitySequence": 0, 24 "discontinuitySequence": 0,
22 "discontinuityStarts": [] 25 "discontinuityStarts": []
23 }
...\ No newline at end of file ...\ No newline at end of file
26 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 8, 12 "duration": 8,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 8, 17 "duration": 8,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 8, 22 "duration": 8,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 "audio-lo": {
7 "English": {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 uri: "englo/prog_index.m3u8"
12 },
13 "Français": {
14 autoselect: true,
15 default: false,
16 language: "fre",
17 uri: "frelo/prog_index.m3u8"
18 },
19 "Espanol": {
20 autoselect: true,
21 default: false,
22 language: "sp",
23 uri: "splo/prog_index.m3u8"
24 }
25 },
26 "audio-hi": {
27 "English": {
28 autoselect: true,
29 default: true,
30 language: "eng",
31 uri: "eng/prog_index.m3u8"
32 },
33 "Français": {
34 autoselect: true,
35 default: false,
36 language: "fre",
37 uri: "fre/prog_index.m3u8"
38 },
39 "Espanol": {
40 autoselect: true,
41 default: false,
42 language: "sp",
43 uri: "sp/prog_index.m3u8"
44 }
45 }
46 },
47 VIDEO: {},
48 "CLOSED-CAPTIONS": {},
49 SUBTITLES: {}
50 },
51 playlists: [{
52 attributes: {
53 "PROGRAM-ID": 1,
54 BANDWIDTH: 195023,
55 CODECS: "mp4a.40.5",
56 AUDIO: "audio-lo",
57 },
58 timeline: 0,
59 uri: "lo/prog_index.m3u8"
60 }, {
61 attributes: {
62 "PROGRAM-ID": 1,
63 BANDWIDTH: 260000,
64 CODECS: "avc1.42e01e,mp4a.40.2",
65 AUDIO: "audio-lo"
66 },
67 timeline: 0,
68 uri: "lo2/prog_index.m3u8"
69 }, {
70 attributes: {
71 "PROGRAM-ID": 1,
72 BANDWIDTH: 591680,
73 CODECS: "mp4a.40.2, avc1.64001e",
74 AUDIO: "audio-hi"
75 },
76 timeline: 0,
77 uri: "hi/prog_index.m3u8"
78 }, {
79 attributes: {
80 "PROGRAM-ID": 1,
81 BANDWIDTH: 650000,
82 CODECS: "avc1.42e01e,mp4a.40.2",
83 AUDIO: "audio-hi"
84 },
85 timeline: 0,
86 uri: "hi2/prog_index.m3u8"
87 }]
88 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="englo/prog_index.m3u8"
3 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="frelo/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="splo/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
7 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
9
10 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
11 lo/prog_index.m3u8
12 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
13 lo2/prog_index.m3u8
14 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e", AUDIO="audio-hi"
15 hi/prog_index.m3u8
16 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-hi"
17 hi2/prog_index.m3u8
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 "audio-lo": {
7 "English": {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 },
12 "Français": {
13 autoselect: true,
14 default: false,
15 language: "fre",
16 uri: "frelo/prog_index.m3u8"
17 },
18 "Espanol": {
19 autoselect: true,
20 default: false,
21 language: "sp",
22 uri: "splo/prog_index.m3u8"
23 }
24 },
25 "audio-hi": {
26 "English": {
27 autoselect: true,
28 default: true,
29 language: "eng",
30 uri: "eng/prog_index.m3u8"
31 },
32 "Français": {
33 autoselect: true,
34 default: false,
35 language: "fre",
36 uri: "fre/prog_index.m3u8"
37 },
38 "Espanol": {
39 autoselect: true,
40 default: false,
41 language: "sp",
42 uri: "sp/prog_index.m3u8"
43 }
44 }
45 },
46 VIDEO: {},
47 "CLOSED-CAPTIONS": {},
48 SUBTITLES: {}
49 },
50 playlists: [{
51 attributes: {
52 "PROGRAM-ID": 1,
53 BANDWIDTH: 195023,
54 CODECS: "mp4a.40.5",
55 AUDIO: "audio-lo",
56 },
57 timeline: 0,
58 uri: "lo/prog_index.m3u8"
59 }, {
60 attributes: {
61 "PROGRAM-ID": 1,
62 BANDWIDTH: 260000,
63 CODECS: "avc1.42e01e,mp4a.40.2",
64 AUDIO: "audio-lo"
65 },
66 timeline: 0,
67 uri: "lo2/prog_index.m3u8"
68 }, {
69 attributes: {
70 "PROGRAM-ID": 1,
71 BANDWIDTH: 591680,
72 CODECS: "mp4a.40.2, avc1.64001e",
73 AUDIO: "audio-hi"
74 },
75 timeline: 0,
76 uri: "hi/prog_index.m3u8"
77 }, {
78 attributes: {
79 "PROGRAM-ID": 1,
80 BANDWIDTH: 650000,
81 CODECS: "avc1.42e01e,mp4a.40.2",
82 AUDIO: "audio-hi"
83 },
84 timeline: 0,
85 uri: "hi2/prog_index.m3u8"
86 }]
87 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES
3 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="frelo/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="splo/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
7 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
9
10 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
11 lo/prog_index.m3u8
12 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
13 lo2/prog_index.m3u8
14 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e", AUDIO="audio-hi"
15 hi/prog_index.m3u8
16 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-hi"
17 hi2/prog_index.m3u8
...@@ -4,21 +4,25 @@ ...@@ -4,21 +4,25 @@
4 "targetDuration": 10, 4 "targetDuration": 10,
5 "segments": [ 5 "segments": [
6 { 6 {
7 "uri": "001.ts" 7 "uri": "001.ts",
8 "timeline": 0
8 }, 9 },
9 { 10 {
10 "uri": "002.ts", 11 "uri": "002.ts",
11 "duration": 9 12 "duration": 9,
13 "timeline": 0
12 }, 14 },
13 { 15 {
14 "uri": "003.ts", 16 "uri": "003.ts",
15 "duration": 7 17 "duration": 7,
18 "timeline": 0
16 }, 19 },
17 { 20 {
18 "uri": "004.ts", 21 "uri": "004.ts",
19 "duration": 10 22 "duration": 10,
23 "timeline": 0
20 } 24 }
21 ], 25 ],
22 "discontinuitySequence": 0, 26 "discontinuitySequence": 0,
23 "discontinuityStarts": [] 27 "discontinuityStarts": []
24 }
...\ No newline at end of file ...\ No newline at end of file
28 }
......
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 aac: {
7 English: {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 uri: "eng/prog_index.m3u8"
12 }
13 }
14 },
15 VIDEO: {
16 "200kbs": {
17 Angle1: {
18 autoselect: true,
19 default: true
20 },
21 Angle2: {
22 autoselect: true,
23 default: false,
24 uri: "Angle2/200kbs/prog_index.m3u8"
25 },
26 Angle3: {
27 autoselect: true,
28 default: false,
29 uri: "Angle3/200kbs/prog_index.m3u8"
30 }
31 },
32 "500kbs": {
33 Angle1: {
34 autoselect: true,
35 default: true
36 },
37 Angle2: {
38 autoselect: true,
39 default: false,
40 uri: "Angle2/500kbs/prog_index.m3u8"
41 },
42 Angle3: {
43 autoselect: true,
44 default: false,
45 uri: "Angle3/500kbs/prog_index.m3u8"
46 }
47 }
48 },
49 "CLOSED-CAPTIONS": {},
50 SUBTITLES: {}
51 },
52 playlists: [{
53 attributes: {
54 "PROGRAM-ID": 1,
55 BANDWIDTH: 300000,
56 CODECS: "mp4a.40.2,avc1.4d401e",
57 AUDIO: "aac",
58 VIDEO: "200kbs"
59 },
60 timeline: 0,
61 uri: "Angle1/200kbs/prog_index.m3u"
62 }, {
63 attributes: {
64 "PROGRAM-ID": 1,
65 BANDWIDTH: 754857,
66 CODECS: "mp4a.40.2,avc1.4d401e",
67 AUDIO: "aac",
68 VIDEO: "500kbs"
69 },
70 timeline: 0,
71 uri: "Angle1/500kbs/prog_index.m3u8"
72 }]
73 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
3 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/200kbs/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/200kbs/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
7 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8"
8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8"
9
10 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8"
11
12 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="200kbs",AUDIO="aac"
13 Angle1/200kbs/prog_index.m3u
14
15 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac"
16 Angle1/500kbs/prog_index.m3u8
...\ No newline at end of file ...\ No newline at end of file
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 10, 29 "duration": 10,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 10, 38 "duration": 10,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,6 +153,7 @@ ...@@ -137,6 +153,7 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 1.4167, 155 "duration": 1.4167,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
...@@ -144,4 +161,4 @@ ...@@ -144,4 +161,4 @@
144 "endList": true, 161 "endList": true,
145 "discontinuitySequence": 0, 162 "discontinuitySequence": 0,
146 "discontinuityStarts": [] 163 "discontinuityStarts": []
147 }
...\ No newline at end of file ...\ No newline at end of file
164 }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 } 10 }
10 ], 11 ],
...@@ -12,4 +13,4 @@ ...@@ -12,4 +13,4 @@
12 "endList": true, 13 "endList": true,
13 "discontinuitySequence": 0, 14 "discontinuitySequence": 0,
14 "discontinuityStarts": [] 15 "discontinuityStarts": []
15 }
...\ No newline at end of file ...\ No newline at end of file
16 }
......
...@@ -5,11 +5,19 @@ ...@@ -5,11 +5,19 @@
5 "attributes": { 5 "attributes": {
6 "PROGRAM-ID": 1 6 "PROGRAM-ID": 1
7 }, 7 },
8 "timeline": 0,
8 "uri": "media.m3u8" 9 "uri": "media.m3u8"
9 }, 10 },
10 { 11 {
12 "timeline": 0,
11 "uri": "media1.m3u8" 13 "uri": "media1.m3u8"
12 } 14 }
13 ], 15 ],
14 "discontinuityStarts": []
15 }
...\ No newline at end of file ...\ No newline at end of file
16 "discontinuityStarts": [],
17 "mediaGroups": {
18 "VIDEO": {},
19 "AUDIO": {},
20 "CLOSED-CAPTIONS": {},
21 "SUBTITLES": {}
22 }
23 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "hls_450k_video.ts" 9 "uri": "hls_450k_video.ts"
9 } 10 }
10 ], 11 ],
...@@ -12,4 +13,4 @@ ...@@ -12,4 +13,4 @@
12 "endList": true, 13 "endList": true,
13 "discontinuitySequence": 0, 14 "discontinuitySequence": 0,
14 "discontinuityStarts": [] 15 "discontinuityStarts": []
15 }
...\ No newline at end of file ...\ No newline at end of file
16 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "http://example.com/00001.ts" 9 "uri": "http://example.com/00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "https://example.com/00002.ts" 14 "uri": "https://example.com/00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "//example.com/00003.ts" 19 "uri": "//example.com/00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "http://example.com/00004.ts" 24 "uri": "http://example.com/00004.ts"
21 } 25 }
22 ], 26 ],
...@@ -24,4 +28,4 @@ ...@@ -24,4 +28,4 @@
24 "endList": true, 28 "endList": true,
25 "discontinuitySequence": 0, 29 "discontinuitySequence": 0,
26 "discontinuityStarts": [] 30 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
31 }
......
1 (function(videojs) {
2 var Component = videojs.getComponent('Component');
3
4 // -----------------
5 // AudioTrackMenuItem
6 // -----------------
7 //
8 var MenuItem = videojs.getComponent('MenuItem');
9
10 var AudioTrackMenuItem = videojs.extend(MenuItem, {
11 constructor: function(player, options) {
12 var track = options.track;
13 var tracks = player.audioTracks();
14
15 options.label = track.label || track.language || 'Unknown';
16 options.selected = track.enabled;
17
18 MenuItem.call(this, player, options);
19
20 this.track = track;
21
22 if (tracks) {
23 var changeHandler = videojs.bind(this, this.handleTracksChange);
24
25 tracks.addEventListener('change', changeHandler);
26 this.on('dispose', function() {
27 tracks.removeEventListener('change', changeHandler);
28 });
29 }
30 },
31
32 handleClick: function(event) {
33 var kind = this.track.kind;
34 var tracks = this.player_.audioTracks();
35
36 MenuItem.prototype.handleClick.call(this, event);
37
38 if (!tracks) return;
39
40 for (var i = 0; i < tracks.length; i++) {
41 var track = tracks[i];
42
43 if (track === this.track) {
44 track.enabled = true;
45 }
46 }
47 },
48
49 handleTracksChange: function(event) {
50 this.selected(this.track.enabled);
51 }
52 });
53
54 Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
55
56 // -----------------
57 // AudioTrackButton
58 // -----------------
59 //
60 var MenuButton = videojs.getComponent('MenuButton');
61
62 var AudioTrackButton = videojs.extend(MenuButton, {
63 constructor: function(player, options) {
64 MenuButton.call(this, player, options);
65 this.el_.setAttribute('aria-label','Audio Menu');
66
67 var tracks = this.player_.audioTracks();
68
69 if (this.items.length <= 1) {
70 this.hide();
71 }
72
73 if (!tracks) {
74 return;
75 }
76
77 var updateHandler = videojs.bind(this, this.update);
78 tracks.addEventListener('removetrack', updateHandler);
79 tracks.addEventListener('addtrack', updateHandler);
80
81 this.player_.on('dispose', function() {
82 tracks.removeEventListener('removetrack', updateHandler);
83 tracks.removeEventListener('addtrack', updateHandler);
84 });
85 },
86
87 buildCSSClass() {
88 return 'vjs-subtitles-button ' + MenuButton.prototype.buildCSSClass.call(this);
89 },
90
91 createItems: function(items) {
92 items = items || [];
93
94 var tracks = this.player_.audioTracks();
95
96 if (!tracks) {
97 return items;
98 }
99
100 for (var i = 0; i < tracks.length; i++) {
101 var track = tracks[i];
102
103 items.push(new AudioTrackMenuItem(this.player_, {
104 'selectable': true,
105 'track': track
106 }));
107 }
108
109 return items;
110 }
111 });
112
113 Component.registerComponent('AudioTrackButton', AudioTrackButton);
114 })(window.videojs);
...@@ -9,28 +9,16 @@ ...@@ -9,28 +9,16 @@
9 <!-- video.js --> 9 <!-- video.js -->
10 <script src="../../node_modules/video.js/dist/video.js"></script> 10 <script src="../../node_modules/video.js/dist/video.js"></script>
11 11
12 <!-- Media Sources plugin -->
13 <script src="../../node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
14
15 <!-- HLS plugin --> 12 <!-- HLS plugin -->
16 <script src="../../src/videojs-hls.js"></script> 13 <script src="../../dist/videojs-contrib-hls.js"></script>
17
18 <!-- m3u8 handling -->
19 <script src="../../src/xhr.js"></script>
20 <script src="../../src/stream.js"></script>
21 <script src="../../src/m3u8/m3u8-parser.js"></script>
22 <script src="../../src/playlist.js"></script>
23 <script src="../../src/playlist-loader.js"></script>
24 14
25 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script> 15 <!-- Track Selector plugin -->
26 <script src="../../src/decrypter.js"></script> 16 <script src="audio-track-selector.js"></script>
27 17
28 <!-- player stats visualization --> 18 <!-- player stats visualization -->
29 <link href="stats.css" rel="stylesheet"> 19 <link href="stats.css" rel="stylesheet">
30 <script src="../switcher/js/vendor/d3.min.js"></script> 20 <script src="../switcher/js/vendor/d3.min.js"></script>
31 21
32 <!-- debugging -->
33 <script src="../../src/bin-utils.js"></script>
34 <style> 22 <style>
35 body { 23 body {
36 font-family: Arial, sans-serif; 24 font-family: Arial, sans-serif;
...@@ -43,13 +31,12 @@ ...@@ -43,13 +31,12 @@
43 padding: 0 5px; 31 padding: 0 5px;
44 margin: 20px 0; 32 margin: 20px 0;
45 } 33 }
46 </style> 34 input {
47 35 margin-top: 15px;
48 <script> 36 min-width: 450px;
49 if (window.location.search === '?flash') { 37 padding: 5px;
50 videojs.options.techOrder = ['flash'];
51 } 38 }
52 </script> 39 </style>
53 40
54 </head> 41 </head>
55 <body> 42 <body>
...@@ -57,18 +44,50 @@ ...@@ -57,18 +44,50 @@
57 <p>The video below is an <a href="https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1">HTTP Live Stream</a>. On desktop browsers other than Safari, the HLS plugin will polyfill support for the format on top of the video.js Flash tech.</p> 44 <p>The video below is an <a href="https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1">HTTP Live Stream</a>. On desktop browsers other than Safari, the HLS plugin will polyfill support for the format on top of the video.js Flash tech.</p>
58 <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p> 45 <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p>
59 </div> 46 </div>
60 <video id="video" 47
61 class="video-js vjs-default-skin" 48 <div id="fixture">
62 height="300" 49 </div>
63 width="600" 50
64 controls> 51 <form id="load-url">
65 <source 52 <label>
66 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8" 53 URL to Load:
67 type="application/x-mpegURL"> 54 <input id="url-to-load" type="url" value="https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8">
68 <source 55 </label>
69 src="http://s3.amazonaws.com/_bc_dml/example-content/bipbop-id3/index.m3u8" 56 <br>
70 type="application/x-mpegURL"> 57 <label>
71 </video> 58 Player Options:
59 <input id="player-options" type="text" value='{}'>
60 </label>
61 <br>
62 <label>
63 URL Content Type:
64 <input id="url-content-type" type="text" value="application/x-mpegURL">
65 </label>
66 <br>
67 <label>
68 Default Caption Track Label (blank for none):
69 <input id="caption-track" type="text" value="">
70 </label>
71 <br>
72 <label>
73 Technology Mode:
74 <input type="radio" name="technology-mode" value="auto" checked> Auto
75 <input type="radio" name="technology-mode" value="html"> HTML
76 <input type="radio" name="technology-mode" value="flash"> Flash
77 </label>
78 <br>
79 <label>
80 Autoplay:
81 <input type="radio" name="autoplay" value="on"> On
82 <input type="radio" name="autoplay" value="off" checked> Off
83 </label>
84
85 <br>
86 <br>
87 <button type="submit">Load</button>
88 </form>
89 <br>
90
72 <section class="stats"> 91 <section class="stats">
73 <div class="player-stats"> 92 <div class="player-stats">
74 <h2>Player Stats</h2> 93 <h2>Player Stats</h2>
...@@ -109,98 +128,181 @@ ...@@ -109,98 +128,181 @@
109 128
110 <script src="stats.js"></script> 129 <script src="stats.js"></script>
111 <script> 130 <script>
112 videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf'; 131 function getCheckedValue(name) {
113 // initialize the player 132 var radios = document.getElementsByName(name);
114 var player = videojs('video').ready(function() { 133 var value;
115 134 for (var i = 0, length = radios.length; i < length; i++) {
116 // ------------ 135 if (radios[i].checked) {
117 // Player Stats 136 value = radios[i].value;
118 // ------------ 137 break;
119 138 }
120 var currentTimeStat = document.querySelector('.current-time-stat'); 139 }
121 var bufferedStat = document.querySelector('.buffered-stat'); 140 return value;
122 var seekableStartStat = document.querySelector('.seekable-start-stat'); 141 }
123 var seekableEndStat = document.querySelector('.seekable-end-stat');
124 var videoBitrateState = document.querySelector('.video-bitrate-stat');
125 var measuredBitrateStat = document.querySelector('.measured-bitrate-stat');
126
127 player.on('timeupdate', function() {
128 currentTimeStat.textContent = player.currentTime().toFixed(1);
129 });
130 142
131 window.setInterval(function() { 143 function createPlayer(cb) {
132 var bufferedText = '', oldStart, oldEnd, i; 144 if (window.stats_timer) {
145 clearInterval(window.stats_timer);
146 }
147 // dispose of existing player
148 if(window.player) {
149 window.player.dispose();
150 }
133 151
134 // buffered 152 // create video element in the dom
135 var buffered = player.buffered(); 153 var video = document.createElement('video');
136 if (buffered.length) { 154 video.id = 'videojs-contrib-hls-player';
137 bufferedText += buffered.start(0) + ' - ' + buffered.end(0); 155 video.className = 'video-js vjs-default-skin';
156 video.setAttribute('controls', true);
157 video.setAttribute('height', 300);
158 video.setAttribute('width', 600);
159 document.querySelector('#fixture').appendChild(video);
160 var techRadios = document.getElementsByName('technology-mode');
161 var techMode = getCheckedValue('technology-mode') || 'auto';
162 var autoplay = getCheckedValue('autoplay') || 'off';
163 var captionTrack = document.getElementById('caption-track').value || "";
164 var url = document.getElementById('url-to-load').value || "";
165 var options = {};
166 // try to parse options from the form
167 try {
168 options = JSON.parse(document.getElementById('player-options').value);
169 } catch(err) {
170 console.log("Reseting options to {}, JSON Parse Error:", err);
171 }
172 if(typeof options.techOrder === 'undefined') {
173 if (techMode === 'html') {
174 options.techOrder = ['html5'];
175 } else if (techMode === 'flash') {
176 options.techOrder = ['flash'];
138 } 177 }
139 for (i = 1; i < buffered.length; i++) { 178 }
140 bufferedText += ', ' + buffered.start(i) + ' - ' + buffered.end(i); 179 if(typeof options.autoplay === 'undefined') {
180 if (autoplay === 'on') {
181 options.autoplay = true;
182 } else if (techMode === 'off') {
183 options.autoplay = false;
141 } 184 }
142 bufferedStat.textContent = bufferedText; 185 }
186 var type = document.getElementById('url-content-type').value;
187 // use the form data to add a src to the player
188 try {
189 window.player = videojs(video.id, options);
190 if(captionTrack) {
191 // hackey way to show captions
192 window.player.on('loadeddata', function(event) {
193 textTracks = this.textTracks().tracks_;
194 for(var i = 0; i < textTracks.length; i++) {
195 if(textTracks[i].label === captionTrack) {
196 console.log("Found matching track, going to show " + captionTrack);
197 textTracks[i].mode = "showing";
198 break;
199 }
200 }
201 });
202 }
203 window.player.src({
204 src: url,
205 type: type
206 });
207
208 cb(player);
209 } catch(err) {
210 console.log("caught an error trying to create and add src to player:", err);
211 }
212 }
213
214 function setup(player) {
215 player.ready(function() {
143 216
144 // seekable 217 // ------------
145 var seekable = player.seekable(); 218 // Audio Track Switcher
146 if (seekable && seekable.length) { 219 // ------------
147 220
148 oldStart = seekableStartStat.textContent; 221 player.controlBar.addChild('AudioTrackButton', {}, 13);
149 if (seekable.start(0).toFixed(1) !== oldStart) { 222
150 seekableStartStat.textContent = seekable.start(0).toFixed(1); 223 // ------------
224 // Player Stats
225 // ------------
226
227 var currentTimeStat = document.querySelector('.current-time-stat');
228 var bufferedStat = document.querySelector('.buffered-stat');
229 var seekableStartStat = document.querySelector('.seekable-start-stat');
230 var seekableEndStat = document.querySelector('.seekable-end-stat');
231 var videoBitrateState = document.querySelector('.video-bitrate-stat');
232 var measuredBitrateStat = document.querySelector('.measured-bitrate-stat');
233
234 player.on('timeupdate', function() {
235 currentTimeStat.textContent = player.currentTime().toFixed(1);
236 });
237
238 window.stats_timer = window.setInterval(function() {
239 var bufferedText = '', oldStart, oldEnd, i;
240
241 // buffered
242 var buffered = player.buffered();
243 if (buffered.length) {
244 bufferedText += buffered.start(0) + ' - ' + buffered.end(0);
151 } 245 }
152 oldEnd = seekableEndStat.textContent; 246 for (i = 1; i < buffered.length; i++) {
153 if (seekable.end(0).toFixed(1) !== oldEnd) { 247 bufferedText += ', ' + buffered.start(i) + ' - ' + buffered.end(i);
154 seekableEndStat.textContent = seekable.end(0).toFixed(1);
155 } 248 }
156 } 249 bufferedStat.textContent = bufferedText;
157 250
158 // bitrates 251 // seekable
159 var playlist = player.tech_.hls.playlists.media(); 252 var seekable = player.seekable();
160 if (playlist && playlist.attributes && playlist.attributes.BANDWIDTH) { 253 if (seekable && seekable.length) {
161 videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, {
162 maximumFractionDigits: 1
163 }) + ' kbps';
164 }
165 if (player.tech_.hls.bandwidth) {
166 measuredBitrateStat.textContent = (player.tech_.hls.bandwidth / 1024).toLocaleString(undefined, {
167 maximumFractionDigits: 1
168 }) + ' kbps';
169 }
170 }, 1000);
171 254
172 var trackEventCount = function(eventName, selector) { 255 oldStart = seekableStartStat.textContent;
173 var count = 0, element = document.querySelector(selector); 256 if (seekable.start(0).toFixed(1) !== oldStart) {
174 player.on(eventName, function() { 257 seekableStartStat.textContent = seekable.start(0).toFixed(1);
175 count++; 258 }
176 element.innerHTML = count; 259 oldEnd = seekableEndStat.textContent;
177 }); 260 if (seekable.end(0).toFixed(1) !== oldEnd) {
178 }; 261 seekableEndStat.textContent = seekable.end(0).toFixed(1);
179 trackEventCount('play', '.play-count'); 262 }
180 trackEventCount('playing', '.playing-count'); 263 }
181 trackEventCount('seeking', '.seeking-count'); 264
182 trackEventCount('seeked', '.seeked-count'); 265 // bitrates
183 266 var playlist = player.tech_.hls.playlists.media();
184 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player); 267 if (playlist && playlist.attributes && playlist.attributes.BANDWIDTH) {
185 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player); 268 videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, {
186 }); 269 maximumFractionDigits: 1
187 270 }) + ' kbps';
188 // ----------- 271 }
189 // Tech Switch 272 if (player.tech_.hls.bandwidth) {
190 // ----------- 273 measuredBitrateStat.textContent = (player.tech_.hls.bandwidth / 1024).toLocaleString(undefined, {
191 274 maximumFractionDigits: 1
192 var techSwitch = document.createElement('a'); 275 }) + ' kbps';
193 techSwitch.className = 'tech-switch'; 276 }
194 if (player.el().querySelector('video')) { 277 }, 1000);
195 techSwitch.href = window.location.origin + window.location.pathname + '?flash';
196 techSwitch.appendChild(document.createTextNode('Switch to the Flash tech'));
197 } else {
198 techSwitch.href = window.location.origin + window.location.pathname;
199 techSwitch.appendChild(document.createTextNode('Stop forcing Flash'));
200 }
201 278
202 document.body.insertBefore(techSwitch, document.querySelector('.stats')); 279 var trackEventCount = function(eventName, selector) {
280 var count = 0, element = document.querySelector(selector);
281 player.on(eventName, function() {
282 count++;
283 element.innerHTML = count;
284 });
285 };
286 trackEventCount('play', '.play-count');
287 trackEventCount('playing', '.playing-count');
288 trackEventCount('seeking', '.seeking-count');
289 trackEventCount('seeked', '.seeked-count');
203 290
291 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player);
292 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player);
293 });
294 }
295
296 (function(window, videojs) {
297 videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf';
298 createPlayer(setup);
299 // hook up the video switcher
300 document.getElementById('load-url').addEventListener('submit', function(event) {
301 event.preventDefault();
302 createPlayer(setup);
303 return false;
304 });
305 }(window, window.videojs));
204 </script> 306 </script>
205 </body> 307 </body>
206 </html> 308 </html>
......