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 6903 additions and 1868 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,13 +11,43 @@ import Stream from './stream'; ...@@ -11,13 +11,43 @@ import Stream from './stream';
11 import m3u8 from './m3u8'; 11 import m3u8 from './m3u8';
12 12
13 /** 13 /**
14 * Returns a new array of segments that is the result of merging
15 * properties from an older list of segments onto an updated
16 * list. No properties on the updated playlist will be overridden.
17 *
18 * @param {Array} original the outdated list of segments
19 * @param {Array} update the updated list of segments
20 * @param {Number=} offset the index of the first update
21 * segment in the original segment list. For non-live playlists,
22 * this should always be zero and does not need to be
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 /**
14 * Returns a new master playlist that is the result of merging an 43 * Returns a new master playlist that is the result of merging an
15 * updated media playlist into the original version. If the 44 * updated media playlist into the original version. If the
16 * updated media playlist does not match any of the playlist 45 * updated media playlist does not match any of the playlist
17 * entries in the original master playlist, null is returned. 46 * entries in the original master playlist, null is returned.
18 * @param master {object} a parsed master M3U8 object 47 *
19 * @param media {object} a parsed media M3U8 object 48 * @param {Object} master a parsed master M3U8 object
20 * @return {object} a new object that represents the original 49 * @param {Object} media a parsed media M3U8 object
50 * @return {Object} a new object that represents the original
21 * master playlist with the updated media playlist merged in, or 51 * master playlist with the updated media playlist merged in, or
22 * null if the merge produced no change. 52 * null if the merge produced no change.
23 */ 53 */
...@@ -26,6 +56,8 @@ const updateMaster = function(master, media) { ...@@ -26,6 +56,8 @@ const updateMaster = function(master, media) {
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(
81 playlist.segments,
49 media.segments, 82 media.segments,
50 media.mediaSequence - 83 media.mediaSequence - playlist.mediaSequence
51 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,43 +104,26 @@ const updateMaster = function(master, media) { ...@@ -57,43 +104,26 @@ 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;
76 let i;
77
78 offset = offset || 0;
79 length = Math.min(original.length, update.length + offset);
80
81 for (i = offset; i < length; i++) {
82 result[i - offset] = mergeOptions(original[i], result[i - offset]);
83 }
84 return result;
85 };
86
87 export default class PlaylistLoader extends Stream {
88 constructor(srcUrl, hls, withCredentials) {
89 super();
90 let loader = this; 117 let loader = this;
118 /* eslint-enable consistent-this */
91 let dispose; 119 let dispose;
92 let mediaUpdateTimeout; 120 let mediaUpdateTimeout;
93 let request; 121 let request;
94 let playlistRequestError; 122 let playlistRequestError;
95 let haveMetadata; 123 let haveMetadata;
96 124
125 PlaylistLoader.prototype.constructor.call(this);
126
97 this.hls_ = hls; 127 this.hls_ = hls;
98 128
99 // a flag that disables "expired time"-tracking this setting has 129 // a flag that disables "expired time"-tracking this setting has
...@@ -121,12 +151,12 @@ export default class PlaylistLoader extends Stream { ...@@ -121,12 +151,12 @@ export default class PlaylistLoader extends Stream {
121 responseText: xhr.responseText, 151 responseText: xhr.responseText,
122 code: (xhr.status >= 500) ? 4 : 2 152 code: (xhr.status >= 500) ? 4 : 2
123 }; 153 };
154
124 loader.trigger('error'); 155 loader.trigger('error');
125 }; 156 };
126 157
127 // update the playlist loader's state in response to a new or 158 // update the playlist loader's state in response to a new or
128 // updated playlist. 159 // updated playlist.
129
130 haveMetadata = function(xhr, url) { 160 haveMetadata = function(xhr, url) {
131 let parser; 161 let parser;
132 let refreshDelay; 162 let refreshDelay;
...@@ -136,6 +166,7 @@ export default class PlaylistLoader extends Stream { ...@@ -136,6 +166,7 @@ export default class PlaylistLoader extends Stream {
136 166
137 // any in-flight request is now finished 167 // any in-flight request is now finished
138 request = null; 168 request = null;
169
139 loader.state = 'HAVE_METADATA'; 170 loader.state = 'HAVE_METADATA';
140 171
141 parser = new m3u8.Parser(); 172 parser = new m3u8.Parser();
...@@ -181,13 +212,19 @@ export default class PlaylistLoader extends Stream { ...@@ -181,13 +212,19 @@ export default class PlaylistLoader extends Stream {
181 * Abort any outstanding work and clean up. 212 * Abort any outstanding work and clean up.
182 */ 213 */
183 loader.dispose = function() { 214 loader.dispose = function() {
215 loader.stopRequest();
216 window.clearTimeout(mediaUpdateTimeout);
217 dispose.call(this);
218 };
219
220 loader.stopRequest = () => {
184 if (request) { 221 if (request) {
185 request.onreadystatechange = null; 222 let oldRequest = request;
186 request.abort(); 223
187 request = null; 224 request = null;
225 oldRequest.onreadystatechange = null;
226 oldRequest.abort();
188 } 227 }
189 window.clearTimeout(mediaUpdateTimeout);
190 dispose.call(this);
191 }; 228 };
192 229
193 /** 230 /**
...@@ -197,12 +234,15 @@ export default class PlaylistLoader extends Stream { ...@@ -197,12 +234,15 @@ export default class PlaylistLoader extends Stream {
197 * specified media playlist. Calling this method while the 234 * specified media playlist. Calling this method while the
198 * loader is in the HAVE_NOTHING causes an error to be emitted 235 * loader is in the HAVE_NOTHING causes an error to be emitted
199 * but otherwise has no effect. 236 * but otherwise has no effect.
200 * @param playlist (optional) {object} the parsed media playlist 237 *
238 * @param {Object=} playlis tthe parsed media playlist
201 * object to switch to 239 * object to switch to
240 * @return {Playlist} the current loaded media
202 */ 241 */
203 loader.media = function(playlist) { 242 loader.media = function(playlist) {
204 let startingState = loader.state; 243 let startingState = loader.state;
205 let mediaChange; 244 let mediaChange;
245
206 // getter 246 // getter
207 if (!playlist) { 247 if (!playlist) {
208 return loader.media_; 248 return loader.media_;
...@@ -237,6 +277,7 @@ export default class PlaylistLoader extends Stream { ...@@ -237,6 +277,7 @@ export default class PlaylistLoader extends Stream {
237 277
238 // trigger media change if the active media has been updated 278 // trigger media change if the active media has been updated
239 if (mediaChange) { 279 if (mediaChange) {
280 loader.trigger('mediachanging');
240 loader.trigger('mediachange'); 281 loader.trigger('mediachange');
241 } 282 }
242 return; 283 return;
...@@ -262,15 +303,23 @@ export default class PlaylistLoader extends Stream { ...@@ -262,15 +303,23 @@ export default class PlaylistLoader extends Stream {
262 } 303 }
263 304
264 // request the new playlist 305 // request the new playlist
306 if (this.media_) {
307 this.trigger('mediachanging');
308 }
265 request = this.hls_.xhr({ 309 request = this.hls_.xhr({
266 uri: resolveUrl(loader.master.uri, playlist.uri), 310 uri: resolveUrl(loader.master.uri, playlist.uri),
267 withCredentials 311 withCredentials
268 }, function(error, request) { 312 }, function(error, req) {
313 // disposed
314 if (!request) {
315 return;
316 }
317
269 if (error) { 318 if (error) {
270 return playlistRequestError(request, playlist.uri, startingState); 319 return playlistRequestError(request, playlist.uri, startingState);
271 } 320 }
272 321
273 haveMetadata(request, playlist.uri); 322 haveMetadata(req, playlist.uri);
274 323
275 // fire loadedmetadata the first time a media playlist is loaded 324 // fire loadedmetadata the first time a media playlist is loaded
276 if (startingState === 'HAVE_MASTER') { 325 if (startingState === 'HAVE_MASTER') {
...@@ -281,12 +330,15 @@ export default class PlaylistLoader extends Stream { ...@@ -281,12 +330,15 @@ export default class PlaylistLoader extends Stream {
281 }); 330 });
282 }; 331 };
283 332
333 /**
334 * set the bandwidth on an xhr to the bandwidth on the playlist
335 */
284 loader.setBandwidth = function(xhr) { 336 loader.setBandwidth = function(xhr) {
285 loader.bandwidth = xhr.bandwidth; 337 loader.bandwidth = xhr.bandwidth;
286 }; 338 };
287 339
288 // In a live list, don't keep track of the expired time until 340 // In a live playlist, don't keep track of the expired time
289 // HLS tells us that "first play" has commenced 341 // until HLS tells us that "first play" has commenced
290 loader.on('firstplay', function() { 342 loader.on('firstplay', function() {
291 this.trackExpiredTime_ = true; 343 this.trackExpiredTime_ = true;
292 }); 344 });
...@@ -302,7 +354,12 @@ export default class PlaylistLoader extends Stream { ...@@ -302,7 +354,12 @@ export default class PlaylistLoader extends Stream {
302 request = this.hls_.xhr({ 354 request = this.hls_.xhr({
303 uri: resolveUrl(loader.master.uri, loader.media().uri), 355 uri: resolveUrl(loader.master.uri, loader.media().uri),
304 withCredentials 356 withCredentials
305 }, function(error, request) { 357 }, function(error, req) {
358 // disposed
359 if (!request) {
360 return;
361 }
362
306 if (error) { 363 if (error) {
307 return playlistRequestError(request, loader.media().uri); 364 return playlistRequestError(request, loader.media().uri);
308 } 365 }
...@@ -310,14 +367,49 @@ export default class PlaylistLoader extends Stream { ...@@ -310,14 +367,49 @@ export default class PlaylistLoader extends Stream {
310 }); 367 });
311 }); 368 });
312 369
370 /**
371 * pause loading of the playlist
372 */
373 loader.pause = () => {
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');
387 }
388 } else {
389 loader.start();
390 }
391 };
392
393 /**
394 * start loading of the playlist
395 */
396 loader.start = () => {
397 loader.started = true;
398
313 // request the specified URL 399 // request the specified URL
314 request = this.hls_.xhr({ 400 request = this.hls_.xhr({
315 uri: srcUrl, 401 uri: srcUrl,
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,16 +473,22 @@ export default class PlaylistLoader extends Stream { ...@@ -368,16 +473,22 @@ 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 };
481 };
482
483 PlaylistLoader.prototype = new Stream();
484
375 /** 485 /**
376 * Update the PlaylistLoader state to reflect the changes in an 486 * Update the PlaylistLoader state to reflect the changes in an
377 * update to the current media playlist. 487 * update to the current media playlist.
378 * @param update {object} the updated media playlist object 488 *
489 * @param {Object} update the updated media playlist object
379 */ 490 */
380 updateMediaPlaylist_(update) { 491 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
381 let outdated; 492 let outdated;
382 let i; 493 let i;
383 let segment; 494 let segment;
...@@ -404,10 +515,10 @@ export default class PlaylistLoader extends Stream { ...@@ -404,10 +515,10 @@ export default class PlaylistLoader extends Stream {
404 // try using precise timing from first segment of the updated 515 // try using precise timing from first segment of the updated
405 // playlist 516 // playlist
406 if (update.segments.length) { 517 if (update.segments.length) {
407 if (update.segments[0].start !== undefined) { 518 if (typeof update.segments[0].start !== 'undefined') {
408 this.expired_ = update.segments[0].start; 519 this.expired_ = update.segments[0].start;
409 return; 520 return;
410 } else if (update.segments[0].end !== undefined) { 521 } else if (typeof update.segments[0].end !== 'undefined') {
411 this.expired_ = update.segments[0].end - update.segments[0].duration; 522 this.expired_ = update.segments[0].end - update.segments[0].duration;
412 return; 523 return;
413 } 524 }
...@@ -428,140 +539,16 @@ export default class PlaylistLoader extends Stream { ...@@ -428,140 +539,16 @@ export default class PlaylistLoader extends Stream {
428 continue; 539 continue;
429 } 540 }
430 541
431 if (segment.end !== undefined) { 542 if (typeof segment.end !== 'undefined') {
432 this.expired_ = segment.end; 543 this.expired_ = segment.end;
433 return; 544 return;
434 } 545 }
435 if (segment.start !== undefined) { 546 if (typeof segment.start !== 'undefined') {
436 this.expired_ = segment.start + segment.duration; 547 this.expired_ = segment.start + segment.duration;
437 return; 548 return;
438 } 549 }
439 this.expired_ += segment.duration; 550 this.expired_ += segment.duration;
440 } 551 }
441 } 552 };
442
443 /**
444 * Determine the index of the segment that contains a specified
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
476 // when the requested position is earlier than the current set of
477 // segments, return the earliest segment index
478 if (time < 0) {
479 return 0;
480 }
481
482 // find segments with known timing information that bound the
483 // target time
484 for (i = 0; i < numSegments; i++) {
485 segment = this.media_.segments[i];
486 if (segment.end) {
487 if (segment.end > time) {
488 knownEnd = segment.end;
489 endIndex = i;
490 break;
491 } else {
492 knownStart = segment.end;
493 startIndex = i + 1;
494 }
495 }
496 }
497
498 // use the bounds we just found and playlist information to
499 // estimate the segment that contains the time we are looking for
500 if (startIndex !== undefined) {
501 // We have a known-start point that is before our desired time so
502 // walk from that point forwards
503 time = time - knownStart;
504 for (i = startIndex; i < (endIndex || numSegments); i++) {
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 553
554 for (i = 0; i < numSegments; i++) { 554 export default PlaylistLoader;
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 }
566 }
567 }
......
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
152 * @param {Number=} endSequence an exclusive upper
133 * boundary for the playlist. Defaults to the playlist media 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,599 +85,13 @@ const safeGetComputedStyle = function(el, property) { ...@@ -220,599 +85,13 @@ const safeGetComputedStyle = function(el, property) {
220 }; 85 };
221 86
222 /** 87 /**
223 * Updates segment with information about its end-point in time and, optionally,
224 * the segment duration if we have enough information to determine a segment duration
225 * accurately.
226 * @param playlist {object} a media playlist object
227 * @param segmentIndex {number} the index of segment we last appended
228 * @param segmentEnd {number} the known of the segment referenced by segmentIndex
229 */
230 const updateSegmentMetadata = function(playlist, segmentIndex, segmentEnd) {
231 if (!playlist) {
232 return;
233 }
234
235 let segment = playlist.segments[segmentIndex];
236 let previousSegment = playlist.segments[segmentIndex - 1];
237
238 if (segmentEnd && segment) {
239 segment.end = segmentEnd;
240
241 // fix up segment durations based on segment end data
242 if (!previousSegment) {
243 // first segment is always has a start time of 0 making its duration
244 // equal to the segment end
245 segment.duration = segment.end;
246 } else if (previousSegment.end) {
247 segment.duration = segment.end - previousSegment.end;
248 }
249 }
250 };
251
252 /**
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
266 let segments = playlist.segments;
267
268 // determine a few boolean values to help make the branch below easier
269 // to read
270 let appendedLastSegment = (segmentIndex === segments.length - 1);
271 let bufferedToEnd = (currentBuffered.length &&
272 segments[segments.length - 1].end <= currentBuffered.end(0));
273
274 // if we've buffered to the end of the video, we need to call endOfStream
275 // so that MediaSources can trigger the `ended` event when it runs out of
276 // buffered data instead of waiting for me
277 return playlist.endList &&
278 mediaSource.readyState === 'open' &&
279 (appendedLastSegment || bufferedToEnd);
280 };
281
282 const parseCodecs = function(codecs) {
283 let result = {
284 codecCount: 0,
285 videoCodec: null,
286 audioProfile: null
287 };
288
289 result.codecCount = codecs.split(',').length;
290 result.codecCount = result.codecCount || 2;
291
292 // parse the video codec but ignore the version
293 result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs);
294 result.videoCodec = result.videoCodec && result.videoCodec[2];
295
296 // parse the last field of the audio codec
297 result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs);
298 result.audioProfile = result.audioProfile && result.audioProfile[2];
299
300 return result;
301 };
302
303 const filterBufferedRanges = function(predicate) {
304 return function(time) {
305 let i;
306 let ranges = [];
307 let tech = this.tech_;
308
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
321 // IE 11 has a bug where it will report a the video as fully buffered
322 // before any data has been loaded. This is a work around where we
323 // report a fully empty buffer until SourceBuffers have been created
324 // which is after a segment has been loaded and transmuxed.
325 if (!this.mediaSource ||
326 (this.mediaSource.mediaSource_ &&
327 !this.mediaSource.mediaSource_.sourceBuffers.length)) {
328 return videojs.createTimeRanges([]);
329 }
330
331 if (buffered && buffered.length) {
332 // Search for a range containing the play-head
333 for (i = 0; i < buffered.length; i++) {
334 if (predicate(buffered.start(i), buffered.end(i), time)) {
335 ranges.push([buffered.start(i), buffered.end(i)]);
336 }
337 }
338 }
339
340 return videojs.createTimeRanges(ranges);
341 };
342 };
343
344 export default class HlsHandler extends Component {
345 constructor(tech, options) {
346 super(tech);
347 let _player;
348
349 // tech.player() is deprecated but setup a reference to HLS for
350 // backwards-compatibility
351 if (tech.options_ && tech.options_.playerId) {
352 _player = videojs(tech.options_.playerId);
353 if (!_player.hls) {
354 Object.defineProperty(_player, 'hls', {
355 get: () => {
356 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
357 return this;
358 }
359 });
360 }
361 }
362 this.tech_ = tech;
363 this.source_ = options.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
369 // start playlist selection at a reasonable bandwidth for
370 // broadband internet
371 // 0.5 Mbps
372 this.bandwidth = options.bandwidth || 4194304;
373 this.bytesReceived = 0;
374
375 // loadingState_ tracks how far along the buffering process we
376 // have been given permission to proceed. There are three possible
377 // values:
378 // - none: do not load playlists or segments
379 // - meta: load playlists but not segments
380 // - segments: load everything
381 this.loadingState_ = 'none';
382 if (this.tech_.preload() !== 'none') {
383 this.loadingState_ = 'meta';
384 }
385
386 // periodically check if new data needs to be downloaded or
387 // buffered data should be appended to the source buffer
388 this.startCheckingBuffer_();
389
390 this.on(this.tech_, 'seeking', function() {
391 this.setCurrentTime(this.tech_.currentTime());
392 });
393 this.on(this.tech_, 'error', function() {
394 this.stopCheckingBuffer_();
395 });
396
397 this.on(this.tech_, 'play', this.play);
398 }
399 src(src) {
400 let oldMediaPlaylist;
401
402 // do nothing if the src is falsey
403 if (!src) {
404 return;
405 }
406
407 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
408
409 // load the MediaSource into the player
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 }
434
435 this.setupSourceBuffer_();
436 this.setupFirstPlay();
437 this.fillBuffer();
438 this.tech_.trigger('loadedmetadata');
439 });
440
441 this.playlists.on('error', () => {
442 this.blacklistCurrentPlaylist_(this.playlists.error);
443 });
444
445 this.playlists.on('loadedplaylist', () => {
446 let updatedPlaylist = this.playlists.media();
447 let seekable;
448
449 if (!updatedPlaylist) {
450 // select the initial variant
451 this.playlists.media(this.selectPlaylist());
452 return;
453 }
454
455 this.updateDuration(this.playlists.media());
456
457 // update seekable
458 seekable = this.seekable();
459 if (this.duration() === Infinity &&
460 seekable.length !== 0) {
461 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
462 }
463
464 oldMediaPlaylist = updatedPlaylist;
465 });
466
467 this.playlists.on('mediachange', () => {
468 this.tech_.trigger({
469 type: 'mediachange',
470 bubbles: true
471 });
472 });
473
474 // do nothing if the tech has been disposed already
475 // this can occur if someone sets the src in player.ready(), for instance
476 if (!this.tech_.el()) {
477 return;
478 }
479
480 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
481 }
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 }
500
501 /**
502 * Blacklist playlists that are known to be codec or
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 *
508 * @param media {object} a media playlist compatible with the current
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 */
514 excludeIncompatibleVariants_(media) {
515 let master = this.playlists.master;
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 }
557
558 setupSourceBuffer_() {
559 let media = this.playlists.media();
560 let mimeType;
561
562 // wait until a media playlist is available and the Media Source is
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 }
584
585 /**
586 * Seek to the latest media position if this is a live video and the
587 * player and video are loaded and initialized.
588 */
589 setupFirstPlay() {
590 let seekable;
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 }
622
623 /**
624 * Begin playing the video.
625 */
626 play() {
627 this.loadingState_ = 'segments';
628
629 if (this.tech_.ended()) {
630 this.tech_.setCurrentTime(0);
631 }
632
633 if (this.tech_.played().length === 0) {
634 return this.setupFirstPlay();
635 }
636
637 // if the viewer has paused and we fell out of the live window,
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 88 * Chooses the appropriate media playlist based on the current
811 * bandwidth estimate and the player size. 89 * bandwidth estimate and the player size.
812 * @return the highest bitrate playlist less than the currently detected 90 *
91 * @return {Playlist} the highest bitrate playlist less than the currently detected
813 * bandwidth, accounting for some amount of bandwidth variance 92 * bandwidth, accounting for some amount of bandwidth variance
814 */ 93 */
815 selectPlaylist() { 94 Hls.STANDARD_PLAYLIST_SELECTOR = function() {
816 let effectiveBitrate; 95 let effectiveBitrate;
817 let sortedPlaylists = this.playlists.master.playlists.slice(); 96 let sortedPlaylists = this.playlists.master.playlists.slice();
818 let bandwidthPlaylists = []; 97 let bandwidthPlaylists = [];
...@@ -848,7 +127,7 @@ export default class HlsHandler extends Component { ...@@ -848,7 +127,7 @@ export default class HlsHandler extends Component {
848 continue; 127 continue;
849 } 128 }
850 129
851 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance; 130 effectiveBitrate = variant.attributes.BANDWIDTH * BANDWIDTH_VARIANCE;
852 131
853 if (effectiveBitrate < this.bandwidth) { 132 if (effectiveBitrate < this.bandwidth) {
854 bandwidthPlaylists.push(variant); 133 bandwidthPlaylists.push(variant);
...@@ -887,660 +166,344 @@ export default class HlsHandler extends Component { ...@@ -887,660 +166,344 @@ export default class HlsHandler extends Component {
887 continue; 166 continue;
888 } 167 }
889 168
890 // since the playlists are sorted, the first variant that has 169 // since the playlists are sorted, the first variant that has
891 // dimensions less than or equal to the player size is the best 170 // dimensions less than or equal to the player size is the best
892 171 let variantResolution = variant.attributes.RESOLUTION;
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 172
1072 if (mediaIndex > 0) { 173 if (variantResolution.width === width &&
1073 segmentTimestampOffset = Hls.Playlist.duration(segmentInfo.playlist, 174 variantResolution.height === height) {
1074 segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_; 175 // if we have the exact resolution as the player use it
1075 } 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
1076 190
1077 if (this.tech_.seeking() && outsideBufferedRanges) { 191 // By only saving variants if they are smaller than the
1078 // If there are discontinuities in the playlist, we can't be sure of anything 192 // previously saved variant, we ensure that we also pick
1079 // related to time so we reset the timestamp offset and start appending data 193 // the highest bandwidth variant that is just-larger-than
1080 // anew on every seek 194 // the video player
1081 if (segmentInfo.playlist.discontinuityStarts.length) { 195 resolutionPlusOne = variant;
1082 segmentInfo.timestampOffset = segmentTimestampOffset; 196 resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION;
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 } 197 }
1094
1095 this.loadSegment(segmentInfo);
1096 } 198 }
1097 199
1098 playlistUriToUrl(segmentRelativeUrl) { 200 // fallback chain of variants
1099 let playListUrl; 201 return resolutionPlusOne ||
1100 202 resolutionBestVariant ||
1101 // resolve the segment URL relative to the playlist 203 bandwidthBestVariant ||
1102 if (this.playlists.media().uri === this.source_.src) { 204 sortedPlaylists[0];
1103 playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl); 205 };
1104 } else {
1105 playListUrl =
1106 resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''),
1107 segmentRelativeUrl);
1108 }
1109 return playListUrl;
1110 }
1111 206
1112 /* 207 // HLS is a source handler, not a tech. Make sure attempts to use it
1113 * Turns segment byterange into a string suitable for use in 208 // as one do not cause exceptions.
1114 * HTTP Range requests 209 Hls.canPlaySource = function() {
1115 */ 210 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
1116 byterangeStr_(byterange) { 211 'your player\'s techOrder.');
1117 let byterangeStart; 212 };
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 213
1127 /* 214 /**
1128 * Defines headers for use in the xhr request for a particular segment. 215 * Whether the browser has built-in HLS support.
1129 */ 216 */
1130 segmentXhrHeaders_(segment) { 217 Hls.supportsNativeHls = (function() {
1131 let headers = {}; 218 let video = document.createElement('video');
1132 219
1133 if ('byterange' in segment) { 220 // native HLS is definitely not supported if HTML5 video isn't
1134 headers.Range = this.byterangeStr_(segment.byterange); 221 if (!videojs.getComponent('Html5').isSupported()) {
1135 } 222 return false;
1136 return headers;
1137 } 223 }
1138 224
1139 /* 225 // HLS manifests can go by many mime-types
1140 * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived. 226 let canPlay = [
1141 * Expects an object with: 227 // Apple santioned
1142 * * `roundTripTime` - the round trip time for the request we're setting the time for 228 'application/vnd.apple.mpegurl',
1143 * * `bandwidth` - the bandwidth we want to set 229 // Apple sanctioned for backwards compatibility
1144 * * `bytesReceived` - amount of bytes downloaded 230 'audio/mpegurl',
1145 * `bandwidth` is the only required property. 231 // Very common
1146 */ 232 'audio/x-mpegurl',
1147 setBandwidth(localXhr) { 233 // Very common
1148 // calculate the download bandwidth 234 'application/x-mpegurl',
1149 this.segmentXhrTime = localXhr.roundTripTime; 235 // Included for completeness
1150 this.bandwidth = localXhr.bandwidth; 236 'video/x-mpegurl',
1151 this.bytesReceived += localXhr.bytesReceived || 0; 237 'video/mpegurl',
1152 238 'application/mpegurl'
1153 this.tech_.trigger('bandwidthupdate'); 239 ];
1154 } 240
241 return canPlay.some(function(canItPlay) {
242 return (/maybe|probably/i).test(video.canPlayType(canItPlay));
243 });
244 }());
1155 245
1156 /* 246 /**
1157 * Blacklists a playlist when an error occurs for a set amount of time 247 * HLS is a source handler, not a tech. Make sure attempts to use it
1158 * making it unavailable for selection by the rendition selection algorithm 248 * as one do not cause exceptions.
1159 * and then forces a new playlist (rendition) selection.
1160 */ 249 */
1161 blacklistCurrentPlaylist_(error) { 250 Hls.isSupported = function() {
1162 let currentPlaylist; 251 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
1163 let nextPlaylist; 252 'your player\'s techOrder.');
1164 253 };
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 254
1181 // Select a new playlist 255 const Component = videojs.getComponent('Component');
1182 nextPlaylist = this.selectPlaylist();
1183 256
1184 if (nextPlaylist) { 257 /**
1185 videojs.log.warn('Problem encountered with the current ' + 258 * The Hls Handler object, where we orchestrate all of the parts
1186 'HLS playlist. Switching to another playlist.'); 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) {
269 super(tech);
1187 270
1188 return this.playlists.media(nextPlaylist); 271 // tech.player() is deprecated but setup a reference to HLS for
1189 } 272 // backwards-compatibility
1190 videojs.log.warn('Problem encountered with the current ' + 273 if (tech.options_ && tech.options_.playerId) {
1191 'HLS playlist. No suitable alternatives found.'); 274 let _player = videojs(tech.options_.playerId);
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 275
1197 loadSegment(segmentInfo) { 276 if (!_player.hasOwnProperty('hls')) {
1198 let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; 277 Object.defineProperty(_player, 'hls', {
1199 let removeToTime = 0; 278 get: () => {
1200 let seekable = this.seekable(); 279 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
1201 let currentTime = this.tech_.currentTime(); 280 return this;
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 } 281 }
1216 282 });
1217 if (removeToTime > 0) {
1218 this.sourceBuffer.remove(0, removeToTime);
1219 } 283 }
1220 } 284 }
1221 285
1222 // if the segment is encrypted, request the key 286 this.options_ = videojs.mergeOptions(videojs.options.hls || {}, options.hls);
1223 if (segment.key) { 287 this.tech_ = tech;
1224 this.fetchKey_(segment); 288 this.source_ = source;
1225 }
1226 289
1227 // request the next segment 290 // start playlist selection at a reasonable bandwidth for
1228 this.segmentXhr_ = this.tech_.hls.xhr({ 291 // broadband internet
1229 uri: segmentInfo.uri, 292 // 0.5 Mbps
1230 responseType: 'arraybuffer', 293 this.bandwidth = this.options_.bandwidth || 4194304;
1231 withCredentials: this.source_.withCredentials, 294 this.bytesReceived = 0;
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 295
1244 // the segment request is no longer outstanding 296 // listen for fullscreenchange events for this player so that we
1245 this.segmentXhr_ = null; 297 // can adjust our quality selection quickly
298 this.on(document, [
299 'fullscreenchange', 'webkitfullscreenchange',
300 'mozfullscreenchange', 'MSFullscreenChange'
301 ], (event) => {
302 let fullscreenElement = document.fullscreenElement ||
303 document.webkitFullscreenElement ||
304 document.mozFullScreenElement ||
305 document.msFullscreenElement;
1246 306
1247 // if a segment request times out, we may have better luck with another playlist 307 if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
1248 if (request.timedout) { 308 this.masterPlaylistController_.fastQualityChange_();
1249 this.bandwidth = 1;
1250 return this.playlists.media(this.selectPlaylist());
1251 } 309 }
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 }); 310 });
1260 }
1261
1262 // stop processing if the request was aborted
1263 if (!request.response) {
1264 return;
1265 }
1266 311
1267 this.lastSegmentLoaded_ = segment; 312 this.on(this.tech_, 'seeking', function() {
1268 this.setBandwidth(request); 313 this.setCurrentTime(this.tech_.currentTime());
1269 314 });
1270 if (segment.key) { 315 this.on(this.tech_, 'error', function() {
1271 segmentInfo.encryptedBytes = new Uint8Array(request.response); 316 if (this.masterPlaylistController_) {
1272 } else { 317 this.masterPlaylistController_.pauseLoading();
1273 segmentInfo.bytes = new Uint8Array(request.response);
1274 } 318 }
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 }); 319 });
1285 320
1286 } 321 this.audioTrackChange_ = () => {
322 this.masterPlaylistController_.useAudio();
323 };
1287 324
1288 drainBuffer() { 325 this.on(this.tech_, 'play', this.play);
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 } 326 }
1302 327
1303 // the pending segment has already been appended and we're waiting 328 /**
1304 // for updateend to fire 329 * called when player.src gets called, handle a new source
1305 if (this.pendingSegment_.buffered) { 330 *
331 * @param {Object} src the source object to handle
332 */
333 src(src) {
334 // do nothing if the src is falsey
335 if (!src) {
1306 return; 336 return;
1307 } 337 }
1308 338
1309 // we can't append more data if the source buffer is busy processing 339 ['withCredentials', 'bandwidth'].forEach((option) => {
1310 // what we've already sent 340 if (typeof this.source_[option] !== 'undefined') {
1311 if (this.sourceBuffer.updating) { 341 this.options_[option] = this.source_[option];
1312 return;
1313 } 342 }
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 }); 343 });
1330 } else if (!segment.key.bytes) { 344 this.options_.url = this.source_.src;
1331 // waiting for the key bytes, try again later 345 this.options_.tech = this.tech_;
1332 return; 346 this.options_.externHls = Hls;
1333 } else if (segmentInfo.decrypter) { 347 this.options_.bandwidth = this.bandwidth;
1334 // decryption is in progress, try again later 348 this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
1335 return; 349 // `this` in selectPlaylist should be the HlsHandler for backwards
1336 } 350 // compatibility with < v2
1337 // if the media sequence is greater than 2^32, the IV will be incorrect 351 this.masterPlaylistController_.selectPlaylist =
1338 // assuming 10s segments, that would be about 1300 years 352 Hls.STANDARD_PLAYLIST_SELECTOR.bind(this);
1339 segIv = segment.key.iv || 353
1340 new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); 354 // re-expose some internal objects for backwards compatibility with < v2
1341 355 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_;
1342 // create a decrypter to incrementally decrypt the segment 356 this.mediaSource = this.masterPlaylistController_.mediaSource;
1343 decrypter = new Hls.Decrypter(segmentInfo.encryptedBytes, 357
1344 segment.key.bytes, 358 // Proxy assignment of some properties to the master playlist
1345 segIv, 359 // controller. Using a custom property for backwards compatibility
1346 function(error, localBytes) { 360 // with < v2
1347 if (error) { 361 Object.defineProperties(this, {
1348 videojs.log.warn(error); 362 selectPlaylist: {
363 get() {
364 return this.masterPlaylistController_.selectPlaylist;
365 },
366 set(selectPlaylist) {
367 this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this);
1349 } 368 }
1350 segmentInfo.bytes = localBytes; 369 },
1351 }); 370 bandwidth: {
1352 segmentInfo.decrypter = decrypter; 371 get() {
1353 return; 372 return this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
373 },
374 set(bandwidth) {
375 this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth;
1354 } 376 }
1355
1356 this.pendingSegment_.buffered = this.tech_.buffered();
1357
1358 if (segmentInfo.timestampOffset !== null) {
1359 this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset;
1360 } 377 }
378 });
1361 379
1362 // the segment is asynchronously added to the current buffered data 380 this.tech_.one('canplay',
1363 this.sourceBuffer.appendBuffer(bytes); 381 this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_));
1364 }
1365 382
1366 updateEndHandler_() { 383 this.masterPlaylistController_.on('sourceopen', () => {
1367 let segmentInfo = this.pendingSegment_; 384 this.tech_.audioTracks().addEventListener('change', this.audioTrackChange_);
1368 let playlist; 385 });
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 386
1381 // In Firefox, the updateend event is triggered for both removing from the buffer and 387 this.masterPlaylistController_.on('audioinfo', (e) => {
1382 // adding to the buffer. To prevent this code from executing on removals, we wait for 388 if (!videojs.browser.IS_FIREFOX ||
1383 // segmentInfo to have a filled in buffered value before we continue processing. 389 !this.audioInfo_ ||
1384 if (!segmentInfo.buffered) { 390 !objectChanged(this.audioInfo_, e.info)) {
391 this.audioInfo_ = e.info;
1385 return; 392 return;
1386 } 393 }
1387 394
1388 this.pendingSegment_ = null; 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';
1389 399
1390 playlist = segmentInfo.playlist; 400 let enabledTrack;
1391 currentMediaIndex = segmentInfo.mediaIndex + 401 let defaultTrack;
1392 (segmentInfo.mediaSequence - playlist.mediaSequence);
1393 currentBuffered = this.findBufferedRange_();
1394 isEndOfStream = detectEndOfStream(playlist, this.mediaSource, currentMediaIndex, currentBuffered);
1395 402
1396 // if we switched renditions don't try to add segment timeline 403 this.masterPlaylistController_.audioTracks_.forEach((t) => {
1397 // information to the playlist 404 if (!defaultTrack && t.default) {
1398 if (segmentInfo.playlist.uri !== this.playlists.media().uri) { 405 defaultTrack = t;
1399 if (isEndOfStream) {
1400 return this.mediaSource.endOfStream();
1401 }
1402 return this.fillBuffer();
1403 } 406 }
1404 407
1405 // when seeking to the beginning of the seekable range, it's 408 if (!enabledTrack && t.enabled) {
1406 // possible that imprecise timing information may cause the seek to 409 enabledTrack = t;
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 } 410 }
411 });
1423 412
1424 timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, 413 // they did not switch audiotracks
1425 this.tech_.buffered()); 414 // blacklist the current playlist
415 if (!enabledTrack.getLoader(this.activeAudioGroup_())) {
416 error = `The rendition that we tried to switch to ${error}` +
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);
426 }
1426 427
1427 // Update segment meta-data (duration and end-point) based on timeline 428 videojs.log.warn(error);
1428 updateSegmentMetadata(playlist, currentMediaIndex, timelineUpdate); 429 this.masterPlaylistController_.useAudio();
430 });
431 this.masterPlaylistController_.on('selectedinitialmedia', () => {
432 // clear current audioTracks
433 this.tech_.clearTracks('audio');
434 this.masterPlaylistController_.audioTracks_.forEach((track) => {
435 this.tech_.audioTracks().addTrack(track);
436 });
437 });
1429 438
1430 // If we decide to signal the end of stream, then we can return instead 439 // the bandwidth of the primary segment loader is our best
1431 // of trying to fetch more segments 440 // estimate of overall bandwidth
1432 if (isEndOfStream) { 441 this.on(this.masterPlaylistController_, 'progress', function() {
1433 return this.mediaSource.endOfStream(); 442 this.bandwidth = this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
1434 } 443 this.tech_.trigger('progress');
444 });
1435 445
1436 if (timelineUpdate !== null || 446 // do nothing if the tech has been disposed already
1437 segmentInfo.buffered.length !== this.tech_.buffered().length) { 447 // this can occur if someone sets the src in player.ready(), for instance
1438 this.updateDuration(playlist); 448 if (!this.tech_.el()) {
1439 // check if it's time to download the next segment
1440 this.fillBuffer();
1441 return; 449 return;
1442 } 450 }
1443 451
1444 // the last segment append must have been entirely in the 452 this.tech_.src(videojs.URL.createObjectURL(
1445 // already buffered time ranges. just buffer forward until we 453 this.masterPlaylistController_.mediaSource));
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 } 454 }
1451 455
1452 /** 456 /**
1453 * Attempt to retrieve the key for a particular media segment. 457 * a helper for grabbing the active audio group from MasterPlaylistController
458 *
459 * @private
1454 */ 460 */
1455 fetchKey_(segment) { 461 activeAudioGroup_() {
1456 let key; 462 return this.masterPlaylistController_.activeAudioGroup();
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 } 463 }
1464 464
1465 settings = this.options_;
1466
1467 /** 465 /**
1468 * Handle a key XHR response. 466 * Begin playing the video.
1469 */ 467 */
1470 receiveKey = (keyRecieved) => { 468 play() {
1471 return (error, request) => { 469 this.masterPlaylistController_.play();
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 } 470 }
1485 471
1486 view = new DataView(request.response); 472 /**
1487 keyRecieved.bytes = new Uint32Array([ 473 * a wrapper around the function in MasterPlaylistController
1488 view.getUint32(0), 474 */
1489 view.getUint32(4), 475 setCurrentTime(currentTime) {
1490 view.getUint32(8), 476 this.masterPlaylistController_.setCurrentTime(currentTime);
1491 view.getUint32(12) 477 }
1492 ]);
1493
1494 // check to see if this allows us to make progress buffering now
1495 this.checkBuffer_();
1496 };
1497 };
1498 478
1499 key = segment.key; 479 /**
480 * a wrapper around the function in MasterPlaylistController
481 */
482 duration() {
483 return this.masterPlaylistController_.duration();
484 }
1500 485
1501 // nothing to do if this segment is unencrypted 486 /**
1502 if (!key) { 487 * a wrapper around the function in MasterPlaylistController
1503 return; 488 */
489 seekable() {
490 return this.masterPlaylistController_.seekable();
1504 } 491 }
1505 492
1506 // request the key if the retry limit hasn't been reached 493 /**
1507 if (!key.bytes && !keyFailed(key)) { 494 * Abort all outstanding work and cleanup.
1508 this.keyXhr_ = this.tech_.hls.xhr({ 495 */
1509 uri: this.playlistUriToUrl(key.uri), 496 dispose() {
1510 responseType: 'arraybuffer', 497 if (this.masterPlaylistController_) {
1511 withCredentials: settings.withCredentials 498 this.masterPlaylistController_.dispose();
1512 }, receiveKey(key));
1513 return;
1514 } 499 }
500 this.tech_.audioTracks().removeEventListener('change', this.audioTrackChange_);
501
502 super.dispose();
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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
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": [] 49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
46 } 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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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": [] 49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
46 } 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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -5,14 +5,17 @@ ...@@ -5,14 +5,17 @@
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 ],
......
...@@ -4,38 +4,47 @@ ...@@ -4,38 +4,47 @@
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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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": [] 49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
46 } 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 ],
......
...@@ -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 ],
......
...@@ -4,10 +4,12 @@ ...@@ -4,10 +4,12 @@
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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
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,19 +4,23 @@ ...@@ -4,19 +4,23 @@
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,
......
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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
...@@ -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": [] 16 "discontinuityStarts": [],
17 "mediaGroups": {
18 "VIDEO": {},
19 "AUDIO": {},
20 "CLOSED-CAPTIONS": {},
21 "SUBTITLES": {}
22 }
15 } 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 ],
......
...@@ -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 ],
......
...@@ -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 ],
......
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,9 +128,97 @@ ...@@ -109,9 +128,97 @@
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;
134 for (var i = 0, length = radios.length; i < length; i++) {
135 if (radios[i].checked) {
136 value = radios[i].value;
137 break;
138 }
139 }
140 return value;
141 }
142
143 function createPlayer(cb) {
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 }
151
152 // create video element in the dom
153 var video = document.createElement('video');
154 video.id = 'videojs-contrib-hls-player';
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'];
177 }
178 }
179 if(typeof options.autoplay === 'undefined') {
180 if (autoplay === 'on') {
181 options.autoplay = true;
182 } else if (techMode === 'off') {
183 options.autoplay = false;
184 }
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() {
216
217 // ------------
218 // Audio Track Switcher
219 // ------------
220
221 player.controlBar.addChild('AudioTrackButton', {}, 13);
115 222
116 // ------------ 223 // ------------
117 // Player Stats 224 // Player Stats
...@@ -128,7 +235,7 @@ ...@@ -128,7 +235,7 @@
128 currentTimeStat.textContent = player.currentTime().toFixed(1); 235 currentTimeStat.textContent = player.currentTime().toFixed(1);
129 }); 236 });
130 237
131 window.setInterval(function() { 238 window.stats_timer = window.setInterval(function() {
132 var bufferedText = '', oldStart, oldEnd, i; 239 var bufferedText = '', oldStart, oldEnd, i;
133 240
134 // buffered 241 // buffered
...@@ -184,23 +291,18 @@ ...@@ -184,23 +291,18 @@
184 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player); 291 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player);
185 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player); 292 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player);
186 }); 293 });
187
188 // -----------
189 // Tech Switch
190 // -----------
191
192 var techSwitch = document.createElement('a');
193 techSwitch.className = 'tech-switch';
194 if (player.el().querySelector('video')) {
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 } 294 }
201 295
202 document.body.insertBefore(techSwitch, document.querySelector('.stats')); 296 (function(window, videojs) {
203 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>
......