2db4b64c by Jon-Carlos Rivera

Merge pull request #568 from videojs/browserify-p5

Browserify p5
2 parents c694b4b7 10562674
1 sudo: false 1 sudo: false
2 language: node_js 2 language: node_js
3 addons:
4 firefox: "latest"
3 node_js: 5 node_js:
4 - 'node' 6 - "stable"
5 - '4.2'
6 - '0.12'
7 - '0.10'
8 notifications: 7 notifications:
9 hipchat: 8 hipchat:
10 rooms: 9 rooms:
......
...@@ -46,10 +46,7 @@ ...@@ -46,10 +46,7 @@
46 </ul> 46 </ul>
47 47
48 <script src="/node_modules/video.js/dist/video.js"></script> 48 <script src="/node_modules/video.js/dist/video.js"></script>
49 <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
50 <script src="/src/videojs-contrib-hls.js"></script>
51 <script src="/dist/videojs-contrib-hls.js"></script> 49 <script src="/dist/videojs-contrib-hls.js"></script>
52 <script src="/src/bin-utils.js"></script>
53 <script> 50 <script>
54 (function(window, videojs) { 51 (function(window, videojs) {
55 var player = window.player = videojs('videojs-contrib-hls-player'); 52 var player = window.player = videojs('videojs-contrib-hls-player');
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 "name": "videojs-contrib-hls", 2 "name": "videojs-contrib-hls",
3 "version": "1.3.5", 3 "version": "1.3.5",
4 "description": "Play back HLS with video.js, even where it's not natively supported", 4 "description": "Play back HLS with video.js, even where it's not natively supported",
5 "main": "es5/stub.js", 5 "main": "es5/videojs-contrib-hls.js",
6 "engines": { 6 "engines": {
7 "node": ">= 0.10.12" 7 "node": ">= 0.10.12"
8 }, 8 },
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
27 "docs": "npm-run-all docs:*", 27 "docs": "npm-run-all docs:*",
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 "prestart": "npm-run-all docs build",
32 "start": "npm-run-all -p start:* watch:*", 32 "start": "npm-run-all -p start:* watch:*",
33 "start:serve": "babel-node scripts/server.js", 33 "start:serve": "babel-node scripts/server.js",
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
40 "preversion": "npm test", 40 "preversion": "npm test",
41 "version": "npm run build", 41 "version": "npm run build",
42 "watch": "npm-run-all -p watch:*", 42 "watch": "npm-run-all -p watch:*",
43 "watch:js": "watchify src/stub.js -t babelify -v -o dist/videojs-contrib-hls.js", 43 "watch:js": "watchify src/videojs-contrib-hls.js -t babelify -v -o dist/videojs-contrib-hls.js",
44 "watch:test": "npm-run-all -p watch:test:*", 44 "watch:test": "npm-run-all -p watch:test:*",
45 "watch:test:js": "node scripts/watch-test.js", 45 "watch:test:js": "node scripts/watch-test.js",
46 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", 46 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
...@@ -72,7 +72,8 @@ ...@@ -72,7 +72,8 @@
72 "scripts", 72 "scripts",
73 "utils", 73 "utils",
74 "test/test-manifests.js", 74 "test/test-manifests.js",
75 "test/test-expected.js" 75 "test/test-expected.js",
76 "src/playlist-loader.js"
76 ] 77 ]
77 }, 78 },
78 "files": [ 79 "files": [
...@@ -88,10 +89,10 @@ ...@@ -88,10 +89,10 @@
88 "utils/" 89 "utils/"
89 ], 90 ],
90 "dependencies": { 91 "dependencies": {
91 "pkcs7": "^0.2.3", 92 "pkcs7": "^0.2.2",
92 "video.js": "^5.0.0", 93 "video.js": "^5.2.1",
93 "videojs-contrib-media-sources": "^2.4.4", 94 "videojs-contrib-media-sources": "^3.0.0",
94 "videojs-swf": "^5.0.1" 95 "videojs-swf": "^5.0.0"
95 }, 96 },
96 "devDependencies": { 97 "devDependencies": {
97 "babel": "^5.8.0", 98 "babel": "^5.8.0",
...@@ -117,12 +118,13 @@ ...@@ -117,12 +118,13 @@
117 "minimist": "^1.2.0", 118 "minimist": "^1.2.0",
118 "npm-run-all": "^1.2.0", 119 "npm-run-all": "^1.2.0",
119 "portscanner": "^1.0.0", 120 "portscanner": "^1.0.0",
120 "qunitjs": "^1.0.0", 121 "qunitjs": "^1.18.0",
121 "serve-static": "^1.10.0", 122 "serve-static": "^1.10.0",
122 "shelljs": "^0.5.3", 123 "shelljs": "^0.5.3",
123 "sinon": "1.10.2", 124 "sinon": "1.10.2",
124 "uglify-js": "^2.5.0", 125 "uglify-js": "^2.5.0",
125 "videojs-standard": "^4.0.0", 126 "videojs-standard": "^4.0.0",
126 "watchify": "^3.6.0" 127 "watchify": "^3.6.0",
128 "webworkify": "^1.1.0"
127 } 129 }
128 } 130 }
......
...@@ -2,7 +2,7 @@ var browserify = require('browserify'); ...@@ -2,7 +2,7 @@ var browserify = require('browserify');
2 var fs = require('fs'); 2 var fs = require('fs');
3 var glob = require('glob'); 3 var glob = require('glob');
4 4
5 glob('test/{playlist*,decryper,m3u8,stub}.test.js', function(err, files) { 5 glob('test/**/*.test.js', function(err, files) {
6 browserify(files) 6 browserify(files)
7 .transform('babelify') 7 .transform('babelify')
8 .bundle() 8 .bundle()
......
...@@ -3,7 +3,7 @@ var fs = require('fs'); ...@@ -3,7 +3,7 @@ var fs = require('fs');
3 var glob = require('glob'); 3 var glob = require('glob');
4 var watchify = require('watchify'); 4 var watchify = require('watchify');
5 5
6 glob('test/{playlist*,decrypter,m3u8,stub}.test.js', function(err, files) { 6 glob('test/**/*.test.js', function(err, files) {
7 var b = browserify(files, { 7 var b = browserify(files, {
8 cache: {}, 8 cache: {},
9 packageCache: {}, 9 packageCache: {},
......
1 (function(window) { 1 const textRange = function(range, i) {
2 var textRange = function(range, i) { 2 return range.start(i) + '-' + range.end(i);
3 return range.start(i) + '-' + range.end(i); 3 };
4 }; 4
5 var module = { 5 const formatHexString = function(e, i) {
6 hexDump: function(data) { 6 let value = e.toString(16);
7 var 7
8 bytes = Array.prototype.slice.call(data), 8 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
9 step = 16, 9 };
10 formatHexString = function(e, i) { 10 const formatAsciiString = function(e) {
11 var value = e.toString(16); 11 if (e >= 0x20 && e < 0x7e) {
12 return "00".substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 12 return String.fromCharCode(e);
13 }, 13 }
14 formatAsciiString = function(e) { 14 return '.';
15 if (e >= 0x20 && e < 0x7e) { 15 };
16 return String.fromCharCode(e); 16
17 } 17 const utils = {
18 return '.'; 18 hexDump(data) {
19 }, 19 let bytes = Array.prototype.slice.call(data);
20 result = '', 20 let step = 16;
21 hex, 21 let result = '';
22 ascii; 22 let hex;
23 for (var j = 0; j < bytes.length / step; j++) { 23 let ascii;
24 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 24
25 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 25 for (let j = 0; j < bytes.length / step; j++) {
26 result += hex + ' ' + ascii + '\n'; 26 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
27 } 27 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
28 return result; 28 result += hex + ' ' + ascii + '\n';
29 }, 29 }
30 tagDump: function(tag) { 30 return result;
31 return module.hexDump(tag.bytes); 31 },
32 }, 32 tagDump(tag) {
33 textRanges: function(ranges) { 33 return utils.hexDump(tag.bytes);
34 var result = '', i; 34 },
35 for (i = 0; i < ranges.length; i++) { 35 textRanges(ranges) {
36 result += textRange(ranges, i) + ' '; 36 let result = '';
37 } 37 let i;
38 return result; 38
39 for (i = 0; i < ranges.length; i++) {
40 result += textRange(ranges, i) + ' ';
39 } 41 }
40 }; 42 return result;
43 }
44 };
41 45
42 window.videojs.Hls.utils = module; 46 export default utils;
43 })(this);
......
...@@ -89,10 +89,9 @@ const precompute = function() { ...@@ -89,10 +89,9 @@ const precompute = function() {
89 decTable[i] = decTable[i].slice(0); 89 decTable[i] = decTable[i].slice(0);
90 } 90 }
91 return tables; 91 return tables;
92 } 92 };
93
94
95 let aesTables = null; 93 let aesTables = null;
94
96 /** 95 /**
97 * Schedule out an AES key for both encryption and decryption. This 96 * Schedule out an AES key for both encryption and decryption. This
98 * 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.
...@@ -116,7 +115,7 @@ export default class AES { ...@@ -116,7 +115,7 @@ export default class AES {
116 */ 115 */
117 // if we have yet to precompute the S-box tables 116 // if we have yet to precompute the S-box tables
118 // do so now 117 // do so now
119 if(!aesTables) { 118 if (!aesTables) {
120 aesTables = precompute(); 119 aesTables = precompute();
121 } 120 }
122 // then make a copy of that object for use 121 // then make a copy of that object for use
......
1 /** 1 /**
2 * A lightweight readable stream implemention that handles event dispatching. 2 * A lightweight readable stream implemention that handles event dispatching.
3 * Objects that inherit from streams should call init in their constructors.
4 */ 3 */
5 export default class Stream { 4 export default class Stream {
6 constructor() { 5 constructor() {
7 this.init();
8 }
9
10 init() {
11 this.listeners = {}; 6 this.listeners = {};
12 } 7 }
13 8
......
1 import m3u8 from './m3u8';
2 import Stream from './stream';
3 import videojs from 'video.js';
4 import {Decrypter, decrypt, AsyncStream} from './decrypter';
5 import Playlist from './playlist';
6 import PlaylistLoader from './playlist-loader';
7 import xhr from './xhr';
8
9
10 if(typeof window.videojs.Hls === 'undefined') {
11 videojs.Hls = {};
12 }
13 videojs.Hls.Stream = Stream;
14 videojs.m3u8 = m3u8;
15 videojs.Hls.decrypt = decrypt;
16 videojs.Hls.Decrypter = Decrypter;
17 videojs.Hls.AsyncStream = AsyncStream;
18 videojs.Hls.xhr = xhr;
19 videojs.Hls.Playlist = Playlist;
20 videojs.Hls.PlaylistLoader = PlaylistLoader;
1 /* 1 /**
2 * videojs-hls 2 * videojs-hls
3 * The main file for the HLS project. 3 * The main file for the HLS project.
4 * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE 4 * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE
5 */ 5 */
6 (function(window, videojs, document, undefined) { 6 import PlaylistLoader from './playlist-loader';
7 'use strict'; 7 import Playlist from './playlist';
8 import xhr from './xhr';
9 import {Decrypter, AsyncStream, decrypt} from './decrypter';
10 import utils from './bin-utils';
11 import {MediaSource, URL} from 'videojs-contrib-media-sources';
12 import m3u8 from './m3u8';
13 import videojs from 'video.js';
14 import resolveUrl from './resolve-url';
15
16 const Hls = {
17 PlaylistLoader,
18 Playlist,
19 Decrypter,
20 AsyncStream,
21 decrypt,
22 utils,
23 xhr
24 };
25
26 // the desired length of video to maintain in the buffer, in seconds
27 Hls.GOAL_BUFFER_LENGTH = 30;
28
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);
8 72
9 var 73 if (edges.some(overlapsCurrentEnd)) {
10 // A fudge factor to apply to advertised playlist bitrates to account for 74 continue;
11 // temporary flucations in client bandwidth 75 }
12 bandwidthVariance = 1.2,
13 blacklistDuration = 5 * 60 * 1000, // 5 minute blacklist
14 TIME_FUDGE_FACTOR = 1 / 30, // Fudge factor to account for TimeRanges rounding
15 Component = videojs.getComponent('Component'),
16 76
17 // The amount of time to wait between checking the state of the buffer 77 // at this point it must be a unique non-shrinking end edge
18 bufferCheckInterval = 500, 78 result.push(end);
79 }
80 }
19 81
20 keyFailed, 82 // we err on the side of caution and return null if didn't find
21 resolveUrl; 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
182 // temporary flucations in client bandwidth
183 const bandwidthVariance = 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;
22 194
23 // returns true if a key has failed to download within a certain amount of retries 195 // returns true if a key has failed to download within a certain amount of retries
24 keyFailed = function(key) { 196 const keyFailed = function(key) {
25 return key.retries && key.retries >= 2; 197 return key.retries && key.retries >= 2;
26 }; 198 };
27 199
28 videojs.Hls = {}; 200 const parseCodecs = function(codecs) {
29 videojs.HlsHandler = videojs.extend(Component, { 201 let result = {
30 constructor: function(tech, options) { 202 codecCount: 0,
31 var self = this, _player; 203 videoCodec: null,
204 audioProfile: null
205 };
32 206
33 Component.call(this, tech); 207 result.codecCount = codecs.split(',').length;
208 result.codecCount = result.codecCount || 2;
209
210 // parse the video codec but ignore the version
211 result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs);
212 result.videoCodec = result.videoCodec && result.videoCodec[2];
213
214 // parse the last field of the audio codec
215 result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs);
216 result.audioProfile = result.audioProfile && result.audioProfile[2];
217
218 return result;
219 };
220
221 const filterBufferedRanges = function(predicate) {
222 return function(time) {
223 let i;
224 let ranges = [];
225 let tech = this.tech_;
226 // !!The order of the next two assignments is important!!
227 // `currentTime` must be equal-to or greater-than the start of the
228 // buffered range. Flash executes out-of-process so, every value can
229 // change behind the scenes from line-to-line. By reading `currentTime`
230 // after `buffered`, we ensure that it is always a current or later
231 // value during playback.
232 let buffered = tech.buffered();
233
234 if (typeof time === 'undefined') {
235 time = tech.currentTime();
236 }
237
238 if (buffered && buffered.length) {
239 // Search for a range containing the play-head
240 for (i = 0; i < buffered.length; i++) {
241 if (predicate(buffered.start(i), buffered.end(i), time)) {
242 ranges.push([buffered.start(i), buffered.end(i)]);
243 }
244 }
245 }
246
247 return videojs.createTimeRanges(ranges);
248 };
249 };
250
251 export default class HlsHandler extends Component {
252 constructor(tech, options) {
253 super(tech);
254 let _player;
34 255
35 // tech.player() is deprecated but setup a reference to HLS for 256 // tech.player() is deprecated but setup a reference to HLS for
36 // backwards-compatibility 257 // backwards-compatibility
...@@ -38,9 +259,9 @@ videojs.HlsHandler = videojs.extend(Component, { ...@@ -38,9 +259,9 @@ videojs.HlsHandler = videojs.extend(Component, {
38 _player = videojs(tech.options_.playerId); 259 _player = videojs(tech.options_.playerId);
39 if (!_player.hls) { 260 if (!_player.hls) {
40 Object.defineProperty(_player, 'hls', { 261 Object.defineProperty(_player, 'hls', {
41 get: function() { 262 get: () => {
42 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.'); 263 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
43 return self; 264 return this;
44 } 265 }
45 }); 266 });
46 } 267 }
...@@ -54,7 +275,8 @@ videojs.HlsHandler = videojs.extend(Component, { ...@@ -54,7 +275,8 @@ videojs.HlsHandler = videojs.extend(Component, {
54 275
55 // start playlist selection at a reasonable bandwidth for 276 // start playlist selection at a reasonable bandwidth for
56 // broadband internet 277 // broadband internet
57 this.bandwidth = options.bandwidth || 4194304; // 0.5 Mbps 278 // 0.5 Mbps
279 this.bandwidth = options.bandwidth || 4194304;
58 this.bytesReceived = 0; 280 this.bytesReceived = 0;
59 281
60 // loadingState_ tracks how far along the buffering process we 282 // loadingState_ tracks how far along the buffering process we
...@@ -81,1409 +303,1200 @@ videojs.HlsHandler = videojs.extend(Component, { ...@@ -81,1409 +303,1200 @@ videojs.HlsHandler = videojs.extend(Component, {
81 303
82 this.on(this.tech_, 'play', this.play); 304 this.on(this.tech_, 'play', this.play);
83 } 305 }
84 }); 306 src(src) {
85 307 let oldMediaPlaylist;
86 // HLS is a source handler, not a tech. Make sure attempts to use it
87 // as one do not cause exceptions.
88 videojs.Hls.canPlaySource = function() {
89 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
90 'your player\'s techOrder.');
91 };
92 308
93 /** 309 // do nothing if the src is falsey
94 * The Source Handler object, which informs video.js what additional 310 if (!src) {
95 * MIME types are supported and sets up playback. It is registered 311 return;
96 * automatically to the appropriate tech based on the capabilities of
97 * the browser it is running in. It is not necessary to use or modify
98 * this object in normal usage.
99 */
100 videojs.HlsSourceHandler = function(mode) {
101 return {
102 canHandleSource: function(srcObj) {
103 return videojs.HlsSourceHandler.canPlayType(srcObj.type);
104 },
105 handleSource: function(source, tech) {
106 if (mode === 'flash') {
107 // We need to trigger this asynchronously to give others the chance
108 // to bind to the event when a source is set at player creation
109 tech.setTimeout(function() {
110 tech.trigger('loadstart');
111 }, 1);
112 }
113 tech.hls = new videojs.HlsHandler(tech, {
114 source: source,
115 mode: mode
116 });
117 tech.hls.src(source.src);
118 return tech.hls;
119 },
120 canPlayType: function(type) {
121 return videojs.HlsSourceHandler.canPlayType(type);
122 } 312 }
123 };
124 };
125 313
126 videojs.HlsSourceHandler.canPlayType = function(type) { 314 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
127 var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
128 315
129 // favor native HLS support if it's available 316 // load the MediaSource into the player
130 if (videojs.Hls.supportsNativeHls) { 317 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
131 return false;
132 }
133 return mpegurlRE.test(type);
134 };
135 318
136 // register source handlers with the appropriate techs 319 this.options_ = {};
137 if (videojs.MediaSource.supportsNativeMediaSources()) { 320 if (typeof this.source_.withCredentials !== 'undefined') {
138 videojs.getComponent('Html5').registerSourceHandler(videojs.HlsSourceHandler('html5')); 321 this.options_.withCredentials = this.source_.withCredentials;
139 } 322 } else if (videojs.options.hls) {
140 if (window.Uint8Array) { 323 this.options_.withCredentials = videojs.options.hls.withCredentials;
141 videojs.getComponent('Flash').registerSourceHandler(videojs.HlsSourceHandler('flash')); 324 }
142 } 325 this.playlists = new Hls.PlaylistLoader(this.source_.src,
143 326 this.options_.withCredentials);
144 // the desired length of video to maintain in the buffer, in seconds
145 videojs.Hls.GOAL_BUFFER_LENGTH = 30;
146 327
147 videojs.HlsHandler.prototype.src = function(src) { 328 this.tech_.one('canplay', this.setupFirstPlay.bind(this));
148 var oldMediaPlaylist;
149 329
150 // do nothing if the src is falsey 330 this.playlists.on('loadedmetadata', () => {
151 if (!src) { 331 oldMediaPlaylist = this.playlists.media();
152 return;
153 }
154 332
155 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); 333 // if this isn't a live video and preload permits, start
334 // downloading segments
335 if (oldMediaPlaylist.endList &&
336 this.tech_.preload() !== 'metadata' &&
337 this.tech_.preload() !== 'none') {
338 this.loadingState_ = 'segments';
339 }
156 340
157 // load the MediaSource into the player 341 this.setupSourceBuffer_();
158 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); 342 this.setupFirstPlay();
343 this.fillBuffer();
344 this.tech_.trigger('loadedmetadata');
345 });
159 346
160 this.options_ = {}; 347 this.playlists.on('error', () => {
161 if (this.source_.withCredentials !== undefined) { 348 this.blacklistCurrentPlaylist_(this.playlists.error);
162 this.options_.withCredentials = this.source_.withCredentials; 349 });
163 } else if (videojs.options.hls) {
164 this.options_.withCredentials = videojs.options.hls.withCredentials;
165 }
166 this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials);
167 350
168 this.tech_.one('canplay', this.setupFirstPlay.bind(this)); 351 this.playlists.on('loadedplaylist', () => {
352 let updatedPlaylist = this.playlists.media();
353 let seekable;
169 354
170 this.playlists.on('loadedmetadata', function() { 355 if (!updatedPlaylist) {
171 oldMediaPlaylist = this.playlists.media(); 356 // select the initial variant
357 this.playlists.media(this.selectPlaylist());
358 return;
359 }
172 360
173 // if this isn't a live video and preload permits, start 361 this.updateDuration(this.playlists.media());
174 // downloading segments
175 if (oldMediaPlaylist.endList &&
176 this.tech_.preload() !== 'metadata' &&
177 this.tech_.preload() !== 'none') {
178 this.loadingState_ = 'segments';
179 }
180 362
181 this.setupSourceBuffer_(); 363 // update seekable
182 this.setupFirstPlay(); 364 seekable = this.seekable();
183 this.fillBuffer(); 365 if (this.duration() === Infinity &&
184 this.tech_.trigger('loadedmetadata'); 366 seekable.length !== 0) {
185 }.bind(this)); 367 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
368 }
186 369
187 this.playlists.on('error', function() { 370 oldMediaPlaylist = updatedPlaylist;
188 this.blacklistCurrentPlaylist_(this.playlists.error); 371 });
189 }.bind(this));
190 372
191 this.playlists.on('loadedplaylist', function() { 373 this.playlists.on('mediachange', () => {
192 var updatedPlaylist = this.playlists.media(), seekable; 374 this.tech_.trigger({
375 type: 'mediachange',
376 bubbles: true
377 });
378 });
193 379
194 if (!updatedPlaylist) { 380 // do nothing if the tech has been disposed already
195 // select the initial variant 381 // this can occur if someone sets the src in player.ready(), for instance
196 this.playlists.media(this.selectPlaylist()); 382 if (!this.tech_.el()) {
197 return; 383 return;
198 } 384 }
199 385
200 this.updateDuration(this.playlists.media()); 386 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
201
202 // update seekable
203 seekable = this.seekable();
204 if (this.duration() === Infinity &&
205 seekable.length !== 0) {
206 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
207 }
208
209 oldMediaPlaylist = updatedPlaylist;
210 }.bind(this));
211
212 this.playlists.on('mediachange', function() {
213 this.tech_.trigger({
214 type: 'mediachange',
215 bubbles: true
216 });
217 }.bind(this));
218
219 // do nothing if the tech has been disposed already
220 // this can occur if someone sets the src in player.ready(), for instance
221 if (!this.tech_.el()) {
222 return;
223 }
224
225 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
226 };
227
228 videojs.HlsHandler.prototype.handleSourceOpen = function() {
229 // Only attempt to create the source buffer if none already exist.
230 // handleSourceOpen is also called when we are "re-opening" a source buffer
231 // after `endOfStream` has been called (in response to a seek for instance)
232 if (!this.sourceBuffer) {
233 this.setupSourceBuffer_();
234 } 387 }
388 handleSourceOpen() {
389 // Only attempt to create the source buffer if none already exist.
390 // handleSourceOpen is also called when we are "re-opening" a source buffer
391 // after `endOfStream` has been called (in response to a seek for instance)
392 if (!this.sourceBuffer) {
393 this.setupSourceBuffer_();
394 }
235 395
236 // if autoplay is enabled, begin playback. This is duplicative of 396 // if autoplay is enabled, begin playback. This is duplicative of
237 // code in video.js but is required because play() must be invoked 397 // code in video.js but is required because play() must be invoked
238 // *after* the media source has opened. 398 // *after* the media source has opened.
239 // NOTE: moving this invocation of play() after 399 // NOTE: moving this invocation of play() after
240 // sourceBuffer.appendBuffer() below caused live streams with 400 // sourceBuffer.appendBuffer() below caused live streams with
241 // autoplay to stall 401 // autoplay to stall
242 if (this.tech_.autoplay()) { 402 if (this.tech_.autoplay()) {
243 this.play(); 403 this.play();
244 }
245 };
246
247 // Search for a likely end time for the segment that was just appened
248 // based on the state of the `buffered` property before and after the
249 // append.
250 // If we found only one such uncommon end-point return it.
251 videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
252 var
253 i, start, end,
254 result = [],
255 edges = [],
256 // In order to qualify as a possible candidate, the end point must:
257 // 1) Not have already existed in the `original` ranges
258 // 2) Not result from the shrinking of a range that already existed
259 // in the `original` ranges
260 // 3) Not be contained inside of a range that existed in `original`
261 overlapsCurrentEnd = function(span) {
262 return (span[0] <= end && span[1] >= end);
263 };
264
265 if (original) {
266 // Save all the edges in the `original` TimeRanges object
267 for (i = 0; i < original.length; i++) {
268 start = original.start(i);
269 end = original.end(i);
270
271 edges.push([start, end]);
272 } 404 }
273 } 405 }
274 406
275 if (update) { 407 /**
276 // Save any end-points in `update` that are not in the `original` 408 * Blacklist playlists that are known to be codec or
277 // TimeRanges object 409 * stream-incompatible with the SourceBuffer configuration. For
278 for (i = 0; i < update.length; i++) { 410 * instance, Media Source Extensions would cause the video element to
279 start = update.start(i); 411 * stall waiting for video data if you switched from a variant with
280 end = update.end(i); 412 * video and audio to an audio-only one.
281 413 *
282 if (edges.some(overlapsCurrentEnd)) { 414 * @param media {object} a media playlist compatible with the current
283 continue; 415 * set of SourceBuffers. Variants in the current master playlist that
416 * do not appear to have compatible codec or stream configurations
417 * will be excluded from the default playlist selection algorithm
418 * indefinitely.
419 */
420 excludeIncompatibleVariants_(media) {
421 let master = this.playlists.master;
422 let codecCount = 2;
423 let videoCodec = null;
424 let audioProfile = null;
425 let codecs;
426
427 if (media.attributes && media.attributes.CODECS) {
428 codecs = parseCodecs(media.attributes.CODECS);
429 videoCodec = codecs.videoCodec;
430 audioProfile = codecs.audioProfile;
431 codecCount = codecs.codecCount;
432 }
433 master.playlists.forEach(function(variant) {
434 let variantCodecs = {
435 codecCount: 2,
436 videoCodec: null,
437 audioProfile: null
438 };
439
440 if (variant.attributes && variant.attributes.CODECS) {
441 variantCodecs = parseCodecs(variant.attributes.CODECS);
284 } 442 }
285 443
286 // at this point it must be a unique non-shrinking end edge 444 // if the streams differ in the presence or absence of audio or
287 result.push(end); 445 // video, they are incompatible
288 } 446 if (variantCodecs.codecCount !== codecCount) {
289 } 447 variant.excludeUntil = Infinity;
448 }
290 449
291 // we err on the side of caution and return null if didn't find 450 // if h.264 is specified on the current playlist, some flavor of
292 // exactly *one* differing end edge in the search above 451 // it must be specified on all compatible variants
293 if (result.length !== 1) { 452 if (variantCodecs.videoCodec !== videoCodec) {
294 return null; 453 variant.excludeUntil = Infinity;
454 }
455 // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
456 // AAC audio in Chrome 46. Don't mix the two.
457 if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
458 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
459 variant.excludeUntil = Infinity;
460 }
461 });
295 } 462 }
296 463
297 return result[0]; 464 setupSourceBuffer_() {
298 }; 465 let media = this.playlists.media();
299 466 let mimeType;
300 var parseCodecs = function(codecs) {
301 var result = {
302 codecCount: 0,
303 videoCodec: null,
304 audioProfile: null
305 };
306
307 result.codecCount = codecs.split(',').length;
308 result.codecCount = result.codecCount || 2;
309
310 // parse the video codec but ignore the version
311 result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs);
312 result.videoCodec = result.videoCodec && result.videoCodec[2];
313
314 // parse the last field of the audio codec
315 result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs);
316 result.audioProfile = result.audioProfile && result.audioProfile[2];
317
318 return result;
319 };
320
321 /**
322 * Blacklist playlists that are known to be codec or
323 * stream-incompatible with the SourceBuffer configuration. For
324 * instance, Media Source Extensions would cause the video element to
325 * stall waiting for video data if you switched from a variant with
326 * video and audio to an audio-only one.
327 *
328 * @param media {object} a media playlist compatible with the current
329 * set of SourceBuffers. Variants in the current master playlist that
330 * do not appear to have compatible codec or stream configurations
331 * will be excluded from the default playlist selection algorithm
332 * indefinitely.
333 */
334 videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) {
335 var
336 master = this.playlists.master,
337 codecCount = 2,
338 videoCodec = null,
339 audioProfile = null,
340 codecs;
341
342 if (media.attributes && media.attributes.CODECS) {
343 codecs = parseCodecs(media.attributes.CODECS);
344 videoCodec = codecs.videoCodec;
345 audioProfile = codecs.audioProfile;
346 codecCount = codecs.codecCount;
347 }
348 master.playlists.forEach(function(variant) {
349 var variantCodecs = {
350 codecCount: 2,
351 videoCodec: null,
352 audioProfile: null
353 };
354 467
355 if (variant.attributes && variant.attributes.CODECS) { 468 // wait until a media playlist is available and the Media Source is
356 variantCodecs = parseCodecs(variant.attributes.CODECS); 469 // attached
357 } 470 if (!media || this.mediaSource.readyState !== 'open') {
358 471 return;
359 // if the streams differ in the presence or absence of audio or
360 // video, they are incompatible
361 if (variantCodecs.codecCount !== codecCount) {
362 variant.excludeUntil = Infinity;
363 } 472 }
364 473
365 // if h.264 is specified on the current playlist, some flavor of 474 // if the codecs were explicitly specified, pass them along to the
366 // it must be specified on all compatible variants 475 // source buffer
367 if (variantCodecs.videoCodec !== videoCodec) { 476 mimeType = 'video/mp2t';
368 variant.excludeUntil = Infinity; 477 if (media.attributes && media.attributes.CODECS) {
369 } 478 mimeType += '; codecs="' + media.attributes.CODECS + '"';
370 // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
371 // AAC audio in Chrome 46. Don't mix the two.
372 if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
373 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
374 variant.excludeUntil = Infinity;
375 } 479 }
376 }); 480 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
377 };
378 481
379 videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { 482 // exclude any incompatible variant streams from future playlist
380 var media = this.playlists.media(), mimeType; 483 // selection
484 this.excludeIncompatibleVariants_(media);
381 485
382 // wait until a media playlist is available and the Media Source is 486 // transition the sourcebuffer to the ended state if we've hit the end of
383 // attached 487 // the playlist
384 if (!media || this.mediaSource.readyState !== 'open') { 488 this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this));
385 return;
386 } 489 }
387 490
388 // if the codecs were explicitly specified, pass them along to the 491 /**
389 // source buffer 492 * Seek to the latest media position if this is a live video and the
390 mimeType = 'video/mp2t'; 493 * player and video are loaded and initialized.
391 if (media.attributes && media.attributes.CODECS) { 494 */
392 mimeType += '; codecs="' + media.attributes.CODECS + '"'; 495 setupFirstPlay() {
393 } 496 let seekable;
394 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType); 497 let media = this.playlists.media();
395 498
396 // exclude any incompatible variant streams from future playlist 499 // check that everything is ready to begin buffering
397 // selection
398 this.excludeIncompatibleVariants_(media);
399 500
400 // transition the sourcebuffer to the ended state if we've hit the end of 501 // 1) the video is a live stream of unknown duration
401 // the playlist 502 if (this.duration() === Infinity &&
402 this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this));
403 };
404 503
405 /** 504 // 2) the player has not played before and is not paused
406 * Seek to the latest media position if this is a live video and the 505 this.tech_.played().length === 0 &&
407 * player and video are loaded and initialized. 506 !this.tech_.paused() &&
408 */
409 videojs.HlsHandler.prototype.setupFirstPlay = function() {
410 var seekable, media;
411 media = this.playlists.media();
412 507
508 // 3) the Media Source and Source Buffers are ready
509 this.sourceBuffer &&
413 510
414 // check that everything is ready to begin buffering 511 // 4) the active media playlist is available
512 media &&
415 513
416 // 1) the video is a live stream of unknown duration 514 // 5) the video element or flash player is in a readyState of
417 if (this.duration() === Infinity && 515 // at least HAVE_FUTURE_DATA
516 this.tech_.readyState() >= 1) {
418 517
419 // 2) the player has not played before and is not paused 518 // trigger the playlist loader to start "expired time"-tracking
420 this.tech_.played().length === 0 && 519 this.playlists.trigger('firstplay');
421 !this.tech_.paused() &&
422 520
423 // 3) the Media Source and Source Buffers are ready 521 // seek to the latest media position for live videos
424 this.sourceBuffer && 522 seekable = this.seekable();
523 if (seekable.length) {
524 this.tech_.setCurrentTime(seekable.end(0));
525 }
526 }
527 }
425 528
426 // 4) the active media playlist is available 529 /**
427 media && 530 * Begin playing the video.
531 */
532 play() {
533 this.loadingState_ = 'segments';
428 534
429 // 5) the video element or flash player is in a readyState of 535 if (this.tech_.ended()) {
430 // at least HAVE_FUTURE_DATA 536 this.tech_.setCurrentTime(0);
431 this.tech_.readyState() >= 1) { 537 }
432 538
433 // trigger the playlist loader to start "expired time"-tracking 539 if (this.tech_.played().length === 0) {
434 this.playlists.trigger('firstplay'); 540 return this.setupFirstPlay();
541 }
435 542
436 // seek to the latest media position for live videos 543 // if the viewer has paused and we fell out of the live window,
437 seekable = this.seekable(); 544 // seek forward to the earliest available position
438 if (seekable.length) { 545 if (this.duration() === Infinity) {
439 this.tech_.setCurrentTime(seekable.end(0)); 546 if (this.tech_.currentTime() < this.seekable().start(0)) {
547 this.tech_.setCurrentTime(this.seekable().start(0));
548 }
440 } 549 }
441 } 550 }
442 };
443 551
444 /** 552 setCurrentTime(currentTime) {
445 * Begin playing the video. 553 let buffered = this.findBufferedRange_();
446 */
447 videojs.HlsHandler.prototype.play = function() {
448 this.loadingState_ = 'segments';
449 554
450 if (this.tech_.ended()) { 555 if (!(this.playlists && this.playlists.media())) {
451 this.tech_.setCurrentTime(0); 556 // return immediately if the metadata is not ready yet
452 } 557 return 0;
558 }
453 559
454 if (this.tech_.played().length === 0) { 560 // it's clearly an edge-case but don't thrown an error if asked to
455 return this.setupFirstPlay(); 561 // seek within an empty playlist
456 } 562 if (!this.playlists.media().segments) {
563 return 0;
564 }
457 565
458 // if the viewer has paused and we fell out of the live window, 566 // if the seek location is already buffered, continue buffering as
459 // seek forward to the earliest available position 567 // usual
460 if (this.duration() === Infinity) { 568 if (buffered && buffered.length) {
461 if (this.tech_.currentTime() < this.seekable().start(0)) { 569 return currentTime;
462 this.tech_.setCurrentTime(this.seekable().start(0));
463 } 570 }
464 }
465 };
466 571
467 videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { 572 // if we are in the middle of appending a segment, let it finish up
468 var 573 if (this.pendingSegment_ && this.pendingSegment_.buffered) {
469 buffered = this.findBufferedRange_(); 574 return currentTime;
575 }
470 576
471 if (!(this.playlists && this.playlists.media())) { 577 this.lastSegmentLoaded_ = null;
472 // return immediately if the metadata is not ready yet
473 return 0;
474 }
475 578
476 // it's clearly an edge-case but don't thrown an error if asked to 579 // cancel outstanding requests and buffer appends
477 // seek within an empty playlist 580 this.cancelSegmentXhr();
478 if (!this.playlists.media().segments) {
479 return 0;
480 }
481 581
482 // if the seek location is already buffered, continue buffering as 582 // abort outstanding key requests, if necessary
483 // usual 583 if (this.keyXhr_) {
484 if (buffered && buffered.length) { 584 this.keyXhr_.aborted = true;
485 return currentTime; 585 this.cancelKeyXhr();
486 } 586 }
487 587
488 // if we are in the middle of appending a segment, let it finish up 588 // begin filling the buffer at the new position
489 if (this.pendingSegment_ && this.pendingSegment_.buffered) { 589 this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
490 return currentTime;
491 } 590 }
492 591
493 this.lastSegmentLoaded_ = null; 592 duration() {
593 let playlists = this.playlists;
494 594
495 // cancel outstanding requests and buffer appends 595 if (playlists) {
496 this.cancelSegmentXhr(); 596 return Hls.Playlist.duration(playlists.media());
497 597 }
498 // abort outstanding key requests, if necessary 598 return 0;
499 if (this.keyXhr_) {
500 this.keyXhr_.aborted = true;
501 this.cancelKeyXhr();
502 } 599 }
503 600
504 // begin filling the buffer at the new position 601 seekable() {
505 this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime)); 602 let media;
506 }; 603 let seekable;
507 604
508 videojs.HlsHandler.prototype.duration = function() { 605 if (!this.playlists) {
509 var playlists = this.playlists; 606 return videojs.createTimeRanges();
510 if (playlists) { 607 }
511 return videojs.Hls.Playlist.duration(playlists.media()); 608 media = this.playlists.media();
512 } 609 if (!media) {
513 return 0; 610 return videojs.createTimeRanges();
514 }; 611 }
515 612
516 videojs.HlsHandler.prototype.seekable = function() { 613 seekable = Hls.Playlist.seekable(media);
517 var media, seekable; 614 if (seekable.length === 0) {
615 return seekable;
616 }
518 617
519 if (!this.playlists) { 618 // if the seekable start is zero, it may be because the player has
520 return videojs.createTimeRanges(); 619 // been paused for a long time and stopped buffering. in that case,
521 } 620 // fall back to the playlist loader's running estimate of expired
522 media = this.playlists.media(); 621 // time
523 if (!media) { 622 if (seekable.start(0) === 0) {
524 return videojs.createTimeRanges(); 623 return videojs.createTimeRanges([[this.playlists.expired_,
525 } 624 this.playlists.expired_ + seekable.end(0)]]);
625 }
526 626
527 seekable = videojs.Hls.Playlist.seekable(media); 627 // seekable has been calculated based on buffering video data so it
528 if (seekable.length === 0) { 628 // can be returned directly
529 return seekable; 629 return seekable;
530 } 630 }
531 631
532 // if the seekable start is zero, it may be because the player has 632 /**
533 // been paused for a long time and stopped buffering. in that case, 633 * Update the player duration
534 // fall back to the playlist loader's running estimate of expired 634 */
535 // time 635 updateDuration(playlist) {
536 if (seekable.start(0) === 0) { 636 let oldDuration = this.mediaSource.duration;
537 return videojs.createTimeRanges([[ 637 let newDuration = Hls.Playlist.duration(playlist);
538 this.playlists.expired_, 638 let setDuration = () => {
539 this.playlists.expired_ + seekable.end(0) 639 this.mediaSource.duration = newDuration;
540 ]]); 640 this.tech_.trigger('durationchange');
541 }
542 641
543 // seekable has been calculated based on buffering video data so it 642 this.mediaSource.removeEventListener('sourceopen', setDuration);
544 // can be returned directly 643 };
545 return seekable;
546 };
547 644
548 /** 645 // if the duration has changed, invalidate the cached value
549 * Update the player duration 646 if (oldDuration !== newDuration) {
550 */ 647 // update the duration
551 videojs.HlsHandler.prototype.updateDuration = function(playlist) { 648 if (this.mediaSource.readyState !== 'open') {
552 var oldDuration = this.mediaSource.duration, 649 this.mediaSource.addEventListener('sourceopen', setDuration);
553 newDuration = videojs.Hls.Playlist.duration(playlist), 650 } else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
554 setDuration = function() {
555 this.mediaSource.duration = newDuration; 651 this.mediaSource.duration = newDuration;
556 this.tech_.trigger('durationchange'); 652 this.tech_.trigger('durationchange');
557 653 }
558 this.mediaSource.removeEventListener('sourceopen', setDuration);
559 }.bind(this);
560
561 // if the duration has changed, invalidate the cached value
562 if (oldDuration !== newDuration) {
563 // update the duration
564 if (this.mediaSource.readyState !== 'open') {
565 this.mediaSource.addEventListener('sourceopen', setDuration);
566 } else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
567 this.mediaSource.duration = newDuration;
568 this.tech_.trigger('durationchange');
569 } 654 }
570 } 655 }
571 };
572 656
573 /** 657 /**
574 * Clear all buffers and reset any state relevant to the current 658 * Clear all buffers and reset any state relevant to the current
575 * source. After this function is called, the tech should be in a 659 * source. After this function is called, the tech should be in a
576 * state suitable for switching to a different video. 660 * state suitable for switching to a different video.
577 */ 661 */
578 videojs.HlsHandler.prototype.resetSrc_ = function() { 662 resetSrc_() {
579 this.cancelSegmentXhr(); 663 this.cancelSegmentXhr();
580 this.cancelKeyXhr(); 664 this.cancelKeyXhr();
581 665
582 if (this.sourceBuffer && this.mediaSource.readyState === 'open') { 666 if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
583 this.sourceBuffer.abort(); 667 this.sourceBuffer.abort();
668 }
584 } 669 }
585 };
586 670
587 videojs.HlsHandler.prototype.cancelKeyXhr = function() { 671 cancelKeyXhr() {
588 if (this.keyXhr_) { 672 if (this.keyXhr_) {
589 this.keyXhr_.onreadystatechange = null; 673 this.keyXhr_.onreadystatechange = null;
590 this.keyXhr_.abort(); 674 this.keyXhr_.abort();
591 this.keyXhr_ = null; 675 this.keyXhr_ = null;
676 }
592 } 677 }
593 };
594 678
595 videojs.HlsHandler.prototype.cancelSegmentXhr = function() { 679 cancelSegmentXhr() {
596 if (this.segmentXhr_) { 680 if (this.segmentXhr_) {
597 // Prevent error handler from running. 681 // Prevent error handler from running.
598 this.segmentXhr_.onreadystatechange = null; 682 this.segmentXhr_.onreadystatechange = null;
599 this.segmentXhr_.abort(); 683 this.segmentXhr_.abort();
600 this.segmentXhr_ = null; 684 this.segmentXhr_ = null;
685 }
686
687 // clear out the segment being processed
688 this.pendingSegment_ = null;
601 } 689 }
602 690
603 // clear out the segment being processed 691 /**
604 this.pendingSegment_ = null; 692 * Abort all outstanding work and cleanup.
605 }; 693 */
694 dispose() {
695 this.stopCheckingBuffer_();
606 696
607 /** 697 if (this.playlists) {
608 * Abort all outstanding work and cleanup. 698 this.playlists.dispose();
609 */ 699 }
610 videojs.HlsHandler.prototype.dispose = function() {
611 this.stopCheckingBuffer_();
612 700
613 if (this.playlists) { 701 this.resetSrc_();
614 this.playlists.dispose(); 702 super.dispose();
615 } 703 }
616 704
617 this.resetSrc_(); 705 /**
618 Component.prototype.dispose.call(this); 706 * Chooses the appropriate media playlist based on the current
619 }; 707 * bandwidth estimate and the player size.
620 708 * @return the highest bitrate playlist less than the currently detected
621 /** 709 * bandwidth, accounting for some amount of bandwidth variance
622 * Chooses the appropriate media playlist based on the current 710 */
623 * bandwidth estimate and the player size. 711 selectPlaylist() {
624 * @return the highest bitrate playlist less than the currently detected 712 let effectiveBitrate;
625 * bandwidth, accounting for some amount of bandwidth variance 713 let sortedPlaylists = this.playlists.master.playlists.slice();
626 */ 714 let bandwidthPlaylists = [];
627 videojs.HlsHandler.prototype.selectPlaylist = function () { 715 let now = +new Date();
628 var 716 let i;
629 effectiveBitrate, 717 let variant;
630 sortedPlaylists = this.playlists.master.playlists.slice(), 718 let bandwidthBestVariant;
631 bandwidthPlaylists = [], 719 let resolutionPlusOne;
632 now = +new Date(), 720 let resolutionPlusOneAttribute;
633 i, 721 let resolutionBestVariant;
634 variant, 722 let width;
635 bandwidthBestVariant, 723 let height;
636 resolutionPlusOne, 724
637 resolutionBestVariant, 725 sortedPlaylists.sort(Hls.comparePlaylistBandwidth);
638 width, 726
639 height; 727 // filter out any playlists that have been excluded due to
640 728 // incompatible configurations or playback errors
641 sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth); 729 sortedPlaylists = sortedPlaylists.filter((localVariant) => {
642 730 if (typeof localVariant.excludeUntil !== 'undefined') {
643 // filter out any playlists that have been excluded due to 731 return now >= localVariant.excludeUntil;
644 // incompatible configurations or playback errors 732 }
645 sortedPlaylists = sortedPlaylists.filter(function(variant) { 733 return true;
646 if (variant.excludeUntil !== undefined) { 734 });
647 return now >= variant.excludeUntil;
648 }
649 return true;
650 });
651 735
652 // filter out any variant that has greater effective bitrate 736 // filter out any variant that has greater effective bitrate
653 // than the current estimated bandwidth 737 // than the current estimated bandwidth
654 i = sortedPlaylists.length; 738 i = sortedPlaylists.length;
655 while (i--) { 739 while (i--) {
656 variant = sortedPlaylists[i]; 740 variant = sortedPlaylists[i];
657 741
658 // ignore playlists without bandwidth information 742 // ignore playlists without bandwidth information
659 if (!variant.attributes || !variant.attributes.BANDWIDTH) { 743 if (!variant.attributes || !variant.attributes.BANDWIDTH) {
660 continue; 744 continue;
661 } 745 }
662 746
663 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance; 747 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
664 748
665 if (effectiveBitrate < this.bandwidth) { 749 if (effectiveBitrate < this.bandwidth) {
666 bandwidthPlaylists.push(variant); 750 bandwidthPlaylists.push(variant);
667 751
668 // since the playlists are sorted in ascending order by 752 // since the playlists are sorted in ascending order by
669 // bandwidth, the first viable variant is the best 753 // bandwidth, the first viable variant is the best
670 if (!bandwidthBestVariant) { 754 if (!bandwidthBestVariant) {
671 bandwidthBestVariant = variant; 755 bandwidthBestVariant = variant;
756 }
672 } 757 }
673 } 758 }
674 }
675
676 i = bandwidthPlaylists.length;
677
678 // sort variants by resolution
679 bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution);
680
681 // forget our old variant from above, or we might choose that in high-bandwidth scenarios
682 // (this could be the lowest bitrate rendition as we go through all of them above)
683 variant = null;
684
685 width = parseInt(getComputedStyle(this.tech_.el()).width, 10);
686 height = parseInt(getComputedStyle(this.tech_.el()).height, 10);
687
688 // iterate through the bandwidth-filtered playlists and find
689 // best rendition by player dimension
690 while (i--) {
691 variant = bandwidthPlaylists[i];
692
693 // ignore playlists without resolution information
694 if (!variant.attributes ||
695 !variant.attributes.RESOLUTION ||
696 !variant.attributes.RESOLUTION.width ||
697 !variant.attributes.RESOLUTION.height) {
698 continue;
699 }
700
701 // since the playlists are sorted, the first variant that has
702 // dimensions less than or equal to the player size is the best
703
704 if (variant.attributes.RESOLUTION.width === width &&
705 variant.attributes.RESOLUTION.height === height) {
706 // if we have the exact resolution as the player use it
707 resolutionPlusOne = null;
708 resolutionBestVariant = variant;
709 break;
710 } else if (variant.attributes.RESOLUTION.width < width &&
711 variant.attributes.RESOLUTION.height < height) {
712 // if both dimensions are less than the player use the
713 // previous (next-largest) variant
714 break;
715 } else if (!resolutionPlusOne ||
716 (variant.attributes.RESOLUTION.width < resolutionPlusOne.attributes.RESOLUTION.width &&
717 variant.attributes.RESOLUTION.height < resolutionPlusOne.attributes.RESOLUTION.height)) {
718 // If we still haven't found a good match keep a
719 // reference to the previous variant for the next loop
720 // iteration
721
722 // By only saving variants if they are smaller than the
723 // previously saved variant, we ensure that we also pick
724 // the highest bandwidth variant that is just-larger-than
725 // the video player
726 resolutionPlusOne = variant;
727 }
728 }
729
730 // fallback chain of variants
731 return resolutionPlusOne || resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0];
732 };
733 759
734 /** 760 i = bandwidthPlaylists.length;
735 * Periodically request new segments and append video data.
736 */
737 videojs.HlsHandler.prototype.checkBuffer_ = function() {
738 // calling this method directly resets any outstanding buffer checks
739 if (this.checkBufferTimeout_) {
740 window.clearTimeout(this.checkBufferTimeout_);
741 this.checkBufferTimeout_ = null;
742 }
743 761
744 this.fillBuffer(); 762 // sort variants by resolution
745 this.drainBuffer(); 763 bandwidthPlaylists.sort(Hls.comparePlaylistResolution);
746 764
747 // wait awhile and try again 765 // forget our old variant from above,
748 this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this), 766 // or we might choose that in high-bandwidth scenarios
749 bufferCheckInterval); 767 // (this could be the lowest bitrate rendition as we go through all of them above)
750 }; 768 variant = null;
751 769
752 /** 770 width = parseInt(getComputedStyle(this.tech_.el()).width, 10);
753 * Setup a periodic task to request new segments if necessary and 771 height = parseInt(getComputedStyle(this.tech_.el()).height, 10);
754 * append bytes into the SourceBuffer.
755 */
756 videojs.HlsHandler.prototype.startCheckingBuffer_ = function() {
757 this.checkBuffer_();
758 };
759 772
760 /** 773 // iterate through the bandwidth-filtered playlists and find
761 * Stop the periodic task requesting new segments and feeding the 774 // best rendition by player dimension
762 * SourceBuffer. 775 while (i--) {
763 */ 776 variant = bandwidthPlaylists[i];
764 videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() {
765 if (this.checkBufferTimeout_) {
766 window.clearTimeout(this.checkBufferTimeout_);
767 this.checkBufferTimeout_ = null;
768 }
769 };
770 777
771 var filterBufferedRanges = function(predicate) { 778 // ignore playlists without resolution information
772 return function(time) { 779 if (!variant.attributes ||
773 var 780 !variant.attributes.RESOLUTION ||
774 i, 781 !variant.attributes.RESOLUTION.width ||
775 ranges = [], 782 !variant.attributes.RESOLUTION.height) {
776 tech = this.tech_, 783 continue;
777 // !!The order of the next two assignments is important!! 784 }
778 // `currentTime` must be equal-to or greater-than the start of the
779 // buffered range. Flash executes out-of-process so, every value can
780 // change behind the scenes from line-to-line. By reading `currentTime`
781 // after `buffered`, we ensure that it is always a current or later
782 // value during playback.
783 buffered = tech.buffered();
784
785
786 if (time === undefined) {
787 time = tech.currentTime();
788 }
789 785
790 if (buffered && buffered.length) { 786 // since the playlists are sorted, the first variant that has
791 // Search for a range containing the play-head 787 // dimensions less than or equal to the player size is the best
792 for (i = 0; i < buffered.length; i++) { 788
793 if (predicate(buffered.start(i), buffered.end(i), time)) { 789 let variantResolution = variant.attributes.RESOLUTION;
794 ranges.push([buffered.start(i), buffered.end(i)]); 790
795 } 791 if (variantResolution.width === width &&
792 variantResolution.height === height) {
793 // if we have the exact resolution as the player use it
794 resolutionPlusOne = null;
795 resolutionBestVariant = variant;
796 break;
797 } else if (variantResolution.width < width &&
798 variantResolution.height < height) {
799 // if both dimensions are less than the player use the
800 // previous (next-largest) variant
801 break;
802 } else if (!resolutionPlusOne ||
803 (variantResolution.width < resolutionPlusOneAttribute.width &&
804 variantResolution.height < resolutionPlusOneAttribute.height)) {
805 // If we still haven't found a good match keep a
806 // reference to the previous variant for the next loop
807 // iteration
808
809 // By only saving variants if they are smaller than the
810 // previously saved variant, we ensure that we also pick
811 // the highest bandwidth variant that is just-larger-than
812 // the video player
813 resolutionPlusOne = variant;
814 resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION;
796 } 815 }
797 } 816 }
798 817
799 return videojs.createTimeRanges(ranges); 818 // fallback chain of variants
800 }; 819 return resolutionPlusOne ||
801 }; 820 resolutionBestVariant ||
821 bandwidthBestVariant ||
822 sortedPlaylists[0];
823 }
802 824
803 /** 825 /**
804 * Attempts to find the buffered TimeRange that contains the specified 826 * Periodically request new segments and append video data.
805 * time, or where playback is currently happening if no specific time 827 */
806 * is specified. 828 checkBuffer_() {
807 * @param time (optional) {number} the time to filter on. Defaults to 829 // calling this method directly resets any outstanding buffer checks
808 * currentTime. 830 if (this.checkBufferTimeout_) {
809 * @return a new TimeRanges object. 831 window.clearTimeout(this.checkBufferTimeout_);
810 */ 832 this.checkBufferTimeout_ = null;
811 videojs.HlsHandler.prototype.findBufferedRange_ = filterBufferedRanges(function(start, end, time) { 833 }
812 return start - TIME_FUDGE_FACTOR <= time &&
813 end + TIME_FUDGE_FACTOR >= time;
814 });
815 834
816 /** 835 this.fillBuffer();
817 * Returns the TimeRanges that begin at or later than the specified 836 this.drainBuffer();
818 * time.
819 * @param time (optional) {number} the time to filter on. Defaults to
820 * currentTime.
821 * @return a new TimeRanges object.
822 */
823 videojs.HlsHandler.prototype.findNextBufferedRange_ = filterBufferedRanges(function(start, end, time) {
824 return start - TIME_FUDGE_FACTOR >= time;
825 });
826 837
827 /** 838 // wait awhile and try again
828 * Determines whether there is enough video data currently in the buffer 839 this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this),
829 * and downloads a new segment if the buffered time is less than the goal. 840 bufferCheckInterval);
830 * @param seekToTime (optional) {number} the offset into the downloaded segment
831 * to seek to, in seconds
832 */
833 videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
834 var
835 tech = this.tech_,
836 currentTime = tech.currentTime(),
837 hasBufferedContent = (this.tech_.buffered().length !== 0),
838 currentBuffered = this.findBufferedRange_(),
839 outsideBufferedRanges = !(currentBuffered && currentBuffered.length),
840 currentBufferedEnd = 0,
841 bufferedTime = 0,
842 segment,
843 segmentInfo,
844 segmentTimestampOffset;
845
846 // if preload is set to "none", do not download segments until playback is requested
847 if (this.loadingState_ !== 'segments') {
848 return;
849 } 841 }
850 842
851 // if a video has not been specified, do nothing 843 /**
852 if (!tech.currentSrc() || !this.playlists) { 844 * Setup a periodic task to request new segments if necessary and
853 return; 845 * append bytes into the SourceBuffer.
846 */
847 startCheckingBuffer_() {
848 this.checkBuffer_();
854 } 849 }
855 850
856 // if there is a request already in flight, do nothing 851 /**
857 if (this.segmentXhr_) { 852 * Stop the periodic task requesting new segments and feeding the
858 return; 853 * SourceBuffer.
854 */
855 stopCheckingBuffer_() {
856 if (this.checkBufferTimeout_) {
857 window.clearTimeout(this.checkBufferTimeout_);
858 this.checkBufferTimeout_ = null;
859 }
859 } 860 }
860 861
861 // wait until the buffer is up to date 862 /**
862 if (this.pendingSegment_) { 863 * Determines whether there is enough video data currently in the buffer
863 return; 864 * and downloads a new segment if the buffered time is less than the goal.
864 } 865 * @param seekToTime (optional) {number} the offset into the downloaded segment
866 * to seek to, in seconds
867 */
868 fillBuffer(mediaIndex) {
869 let tech = this.tech_;
870 let currentTime = tech.currentTime();
871 let hasBufferedContent = (this.tech_.buffered().length !== 0);
872 let currentBuffered = this.findBufferedRange_();
873 let outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
874 let currentBufferedEnd = 0;
875 let bufferedTime = 0;
876 let segment;
877 let segmentInfo;
878 let segmentTimestampOffset;
879
880 // if preload is set to "none", do not download segments until playback is requested
881 if (this.loadingState_ !== 'segments') {
882 return;
883 }
884
885 // if a video has not been specified, do nothing
886 if (!tech.currentSrc() || !this.playlists) {
887 return;
888 }
889
890 // if there is a request already in flight, do nothing
891 if (this.segmentXhr_) {
892 return;
893 }
865 894
866 // if no segments are available, do nothing 895 // wait until the buffer is up to date
867 if (this.playlists.state === "HAVE_NOTHING" || 896 if (this.pendingSegment_) {
868 !this.playlists.media() || 897 return;
869 !this.playlists.media().segments) { 898 }
870 return;
871 }
872 899
873 // if a playlist switch is in progress, wait for it to finish 900 // if no segments are available, do nothing
874 if (this.playlists.state === 'SWITCHING_MEDIA') { 901 if (this.playlists.state === 'HAVE_NOTHING' ||
875 return; 902 !this.playlists.media() ||
876 } 903 !this.playlists.media().segments) {
904 return;
905 }
877 906
878 if (mediaIndex === undefined) { 907 // if a playlist switch is in progress, wait for it to finish
879 if (currentBuffered && currentBuffered.length) { 908 if (this.playlists.state === 'SWITCHING_MEDIA') {
880 currentBufferedEnd = currentBuffered.end(0); 909 return;
881 mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd); 910 }
882 bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
883 911
884 // if there is plenty of content in the buffer and we're not 912 if (typeof mediaIndex === 'undefined') {
885 // seeking, relax for awhile 913 if (currentBuffered && currentBuffered.length) {
886 if (bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { 914 currentBufferedEnd = currentBuffered.end(0);
887 return; 915 mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd);
916 bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
917
918 // if there is plenty of content in the buffer and we're not
919 // seeking, relax for awhile
920 if (bufferedTime >= Hls.GOAL_BUFFER_LENGTH) {
921 return;
922 }
923 } else {
924 mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
888 } 925 }
889 } else {
890 mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
891 } 926 }
892 } 927 segment = this.playlists.media().segments[mediaIndex];
893 segment = this.playlists.media().segments[mediaIndex];
894 928
895 // if the video has finished downloading 929 // if the video has finished downloading
896 if (!segment) { 930 if (!segment) {
897 return; 931 return;
898 } 932 }
899 933
900 // we have entered a state where we are fetching the same segment, 934 // we have entered a state where we are fetching the same segment,
901 // try to walk forward 935 // try to walk forward
902 if (this.lastSegmentLoaded_ && 936 if (this.lastSegmentLoaded_ &&
903 this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) && 937 this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) &&
904 this.lastSegmentLoaded_.byterange === segment.byterange) { 938 this.lastSegmentLoaded_.byterange === segment.byterange) {
905 return this.fillBuffer(mediaIndex + 1); 939 return this.fillBuffer(mediaIndex + 1);
906 } 940 }
907 941
908 // package up all the work to append the segment 942 // package up all the work to append the segment
909 segmentInfo = { 943 segmentInfo = {
910 // resolve the segment URL relative to the playlist 944 // resolve the segment URL relative to the playlist
911 uri: this.playlistUriToUrl(segment.uri), 945 uri: this.playlistUriToUrl(segment.uri),
912 // the segment's mediaIndex & mediaSequence at the time it was requested 946 // the segment's mediaIndex & mediaSequence at the time it was requested
913 mediaIndex: mediaIndex, 947 mediaIndex,
914 mediaSequence: this.playlists.media().mediaSequence, 948 mediaSequence: this.playlists.media().mediaSequence,
915 // the segment's playlist 949 // the segment's playlist
916 playlist: this.playlists.media(), 950 playlist: this.playlists.media(),
917 // The state of the buffer when this segment was requested 951 // The state of the buffer when this segment was requested
918 currentBufferedEnd: currentBufferedEnd, 952 currentBufferedEnd,
919 // unencrypted bytes of the segment 953 // unencrypted bytes of the segment
920 bytes: null, 954 bytes: null,
921 // when a key is defined for this segment, the encrypted bytes 955 // when a key is defined for this segment, the encrypted bytes
922 encryptedBytes: null, 956 encryptedBytes: null,
923 // optionally, the decrypter that is unencrypting the segment 957 // optionally, the decrypter that is unencrypting the segment
924 decrypter: null, 958 decrypter: null,
925 // the state of the buffer before a segment is appended will be 959 // the state of the buffer before a segment is appended will be
926 // stored here so that the actual segment duration can be 960 // stored here so that the actual segment duration can be
927 // determined after it has been appended 961 // determined after it has been appended
928 buffered: null, 962 buffered: null,
929 // The target timestampOffset for this segment when we append it 963 // The target timestampOffset for this segment when we append it
930 // to the source buffer 964 // to the source buffer
931 timestampOffset: null 965 timestampOffset: null
932 }; 966 };
933 967
934 if (mediaIndex > 0) { 968 if (mediaIndex > 0) {
935 segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, 969 segmentTimestampOffset = Hls.Playlist.duration(segmentInfo.playlist,
936 segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_; 970 segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_;
937 } 971 }
938 972
939 if (this.tech_.seeking() && outsideBufferedRanges) { 973 if (this.tech_.seeking() && outsideBufferedRanges) {
940 // If there are discontinuities in the playlist, we can't be sure of anything 974 // If there are discontinuities in the playlist, we can't be sure of anything
941 // related to time so we reset the timestamp offset and start appending data 975 // related to time so we reset the timestamp offset and start appending data
942 // anew on every seek 976 // anew on every seek
943 if (segmentInfo.playlist.discontinuityStarts.length) { 977 if (segmentInfo.playlist.discontinuityStarts.length) {
978 segmentInfo.timestampOffset = segmentTimestampOffset;
979 }
980 } else if (segment.discontinuity && currentBuffered.length) {
981 // If we aren't seeking and are crossing a discontinuity, we should set
982 // timestampOffset for new segments to be appended the end of the current
983 // buffered time-range
984 segmentInfo.timestampOffset = currentBuffered.end(0);
985 } else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
986 // If we are trying to play at a position that is not zero but we aren't
987 // currently seeking according to the video element
944 segmentInfo.timestampOffset = segmentTimestampOffset; 988 segmentInfo.timestampOffset = segmentTimestampOffset;
945 } 989 }
946 } else if (segment.discontinuity && currentBuffered.length) { 990
947 // If we aren't seeking and are crossing a discontinuity, we should set 991 this.loadSegment(segmentInfo);
948 // timestampOffset for new segments to be appended the end of the current
949 // buffered time-range
950 segmentInfo.timestampOffset = currentBuffered.end(0);
951 } else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
952 // If we are trying to play at a position that is not zero but we aren't
953 // currently seeking according to the video element
954 segmentInfo.timestampOffset = segmentTimestampOffset;
955 } 992 }
956 993
957 this.loadSegment(segmentInfo); 994 playlistUriToUrl(segmentRelativeUrl) {
958 }; 995 let playListUrl;
959 996
960 videojs.HlsHandler.prototype.playlistUriToUrl = function(segmentRelativeUrl) { 997 // resolve the segment URL relative to the playlist
961 var playListUrl; 998 if (this.playlists.media().uri === this.source_.src) {
962 // resolve the segment URL relative to the playlist 999 playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl);
963 if (this.playlists.media().uri === this.source_.src) { 1000 } else {
964 playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl); 1001 playListUrl =
965 } else { 1002 resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''),
966 playListUrl = resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), segmentRelativeUrl); 1003 segmentRelativeUrl);
1004 }
1005 return playListUrl;
967 } 1006 }
968 return playListUrl;
969 };
970 1007
971 /* Turns segment byterange into a string suitable for use in 1008 /*
972 * HTTP Range requests 1009 * Turns segment byterange into a string suitable for use in
973 */ 1010 * HTTP Range requests
974 videojs.HlsHandler.prototype.byterangeStr_ = function(byterange) { 1011 */
975 var byterangeStart, byterangeEnd; 1012 byterangeStr_(byterange) {
1013 let byterangeStart;
1014 let byterangeEnd;
976 1015
977 // `byterangeEnd` is one less than `offset + length` because the HTTP range 1016 // `byterangeEnd` is one less than `offset + length` because the HTTP range
978 // header uses inclusive ranges 1017 // header uses inclusive ranges
979 byterangeEnd = byterange.offset + byterange.length - 1; 1018 byterangeEnd = byterange.offset + byterange.length - 1;
980 byterangeStart = byterange.offset; 1019 byterangeStart = byterange.offset;
981 return "bytes=" + byterangeStart + "-" + byterangeEnd; 1020 return 'bytes=' + byterangeStart + '-' + byterangeEnd;
982 };
983
984 /* Defines headers for use in the xhr request for a particular segment.
985 */
986 videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) {
987 var headers = {};
988 if ('byterange' in segment) {
989 headers['Range'] = this.byterangeStr_(segment.byterange);
990 } 1021 }
991 return headers;
992 };
993 1022
994 /* 1023 /*
995 * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived. 1024 * Defines headers for use in the xhr request for a particular segment.
996 * Expects an object with: 1025 */
997 * * `roundTripTime` - the round trip time for the request we're setting the time for 1026 segmentXhrHeaders_(segment) {
998 * * `bandwidth` - the bandwidth we want to set 1027 let headers = {};
999 * * `bytesReceived` - amount of bytes downloaded
1000 * `bandwidth` is the only required property.
1001 */
1002 videojs.HlsHandler.prototype.setBandwidth = function(xhr) {
1003 // calculate the download bandwidth
1004 this.segmentXhrTime = xhr.roundTripTime;
1005 this.bandwidth = xhr.bandwidth;
1006 this.bytesReceived += xhr.bytesReceived || 0;
1007 1028
1008 this.tech_.trigger('bandwidthupdate'); 1029 if ('byterange' in segment) {
1009 }; 1030 headers.Range = this.byterangeStr_(segment.byterange);
1031 }
1032 return headers;
1033 }
1010 1034
1011 /* 1035 /*
1012 * Blacklists a playlist when an error occurs for a set amount of time 1036 * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived.
1013 * making it unavailable for selection by the rendition selection algorithm 1037 * Expects an object with:
1014 * and then forces a new playlist (rendition) selection. 1038 * * `roundTripTime` - the round trip time for the request we're setting the time for
1015 */ 1039 * * `bandwidth` - the bandwidth we want to set
1016 videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) { 1040 * * `bytesReceived` - amount of bytes downloaded
1017 var currentPlaylist, nextPlaylist; 1041 * `bandwidth` is the only required property.
1018 1042 */
1019 // If the `error` was generated by the playlist loader, it will contain 1043 setBandwidth(localXhr) {
1020 // the playlist we were trying to load (but failed) and that should be 1044 // calculate the download bandwidth
1021 // blacklisted instead of the currently selected playlist which is likely 1045 this.segmentXhrTime = localXhr.roundTripTime;
1022 // out-of-date in this scenario 1046 this.bandwidth = localXhr.bandwidth;
1023 currentPlaylist = error.playlist || this.playlists.media(); 1047 this.bytesReceived += localXhr.bytesReceived || 0;
1024 1048
1025 // If there is no current playlist, then an error occurred while we were 1049 this.tech_.trigger('bandwidthupdate');
1026 // trying to load the master OR while we were disposing of the tech
1027 if (!currentPlaylist) {
1028 this.error = error;
1029 return this.mediaSource.endOfStream('network');
1030 } 1050 }
1031 1051
1032 // Blacklist this playlist 1052 /*
1033 currentPlaylist.excludeUntil = Date.now() + blacklistDuration; 1053 * Blacklists a playlist when an error occurs for a set amount of time
1054 * making it unavailable for selection by the rendition selection algorithm
1055 * and then forces a new playlist (rendition) selection.
1056 */
1057 blacklistCurrentPlaylist_(error) {
1058 let currentPlaylist;
1059 let nextPlaylist;
1060
1061 // If the `error` was generated by the playlist loader, it will contain
1062 // the playlist we were trying to load (but failed) and that should be
1063 // blacklisted instead of the currently selected playlist which is likely
1064 // out-of-date in this scenario
1065 currentPlaylist = error.playlist || this.playlists.media();
1066
1067 // If there is no current playlist, then an error occurred while we were
1068 // trying to load the master OR while we were disposing of the tech
1069 if (!currentPlaylist) {
1070 this.error = error;
1071 return this.mediaSource.endOfStream('network');
1072 }
1073
1074 // Blacklist this playlist
1075 currentPlaylist.excludeUntil = Date.now() + blacklistDuration;
1034 1076
1035 // Select a new playlist 1077 // Select a new playlist
1036 nextPlaylist = this.selectPlaylist(); 1078 nextPlaylist = this.selectPlaylist();
1037 1079
1038 if (nextPlaylist) { 1080 if (nextPlaylist) {
1039 videojs.log.warn('Problem encountered with the current HLS playlist. Switching to another playlist.'); 1081 videojs.log.warn('Problem encountered with the current ' +
1082 'HLS playlist. Switching to another playlist.');
1040 1083
1041 return this.playlists.media(nextPlaylist); 1084 return this.playlists.media(nextPlaylist);
1042 } else { 1085 }
1043 videojs.log.warn('Problem encountered with the current HLS playlist. No suitable alternatives found.'); 1086 videojs.log.warn('Problem encountered with the current ' +
1087 'HLS playlist. No suitable alternatives found.');
1044 // We have no more playlists we can select so we must fail 1088 // We have no more playlists we can select so we must fail
1045 this.error = error; 1089 this.error = error;
1046 return this.mediaSource.endOfStream('network'); 1090 return this.mediaSource.endOfStream('network');
1047 } 1091 }
1048 };
1049 1092
1050 videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { 1093 loadSegment(segmentInfo) {
1051 var 1094 let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
1052 self = this, 1095 let removeToTime = 0;
1053 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex], 1096 let seekable = this.seekable();
1054 removeToTime = 0, 1097
1055 seekable = this.seekable(); 1098 // Chrome has a hard limit of 150mb of
1056 1099 // buffer and a very conservative "garbage collector"
1057 // Chrome has a hard limit of 150mb of buffer and a very conservative "garbage collector" 1100 // We manually clear out the old buffer to ensure
1058 // We manually clear out the old buffer to ensure we don't trigger the QuotaExceeded error 1101 // we don't trigger the QuotaExceeded error
1059 // on the source buffer during subsequent appends 1102 // on the source buffer during subsequent appends
1060 if (this.sourceBuffer && !this.sourceBuffer.updating) { 1103 if (this.sourceBuffer && !this.sourceBuffer.updating) {
1061 // If we have a seekable range use that as the limit for what can be removed safely 1104 // If we have a seekable range use that as the limit for what can be removed safely
1062 // otherwise remove anything older than 1 minute before the current play head 1105 // otherwise remove anything older than 1 minute before the current play head
1063 if (seekable.length && seekable.start(0) > 0) { 1106 if (seekable.length && seekable.start(0) > 0) {
1064 removeToTime = seekable.start(0); 1107 removeToTime = seekable.start(0);
1065 } else { 1108 } else {
1066 removeToTime = this.tech_.currentTime() - 60; 1109 removeToTime = this.tech_.currentTime() - 60;
1067 } 1110 }
1068 1111
1069 if (removeToTime > 0) { 1112 if (removeToTime > 0) {
1070 this.sourceBuffer.remove(0, removeToTime); 1113 this.sourceBuffer.remove(0, removeToTime);
1114 }
1071 } 1115 }
1072 }
1073
1074 // if the segment is encrypted, request the key
1075 if (segment.key) {
1076 this.fetchKey_(segment);
1077 }
1078 1116
1079 // request the next segment 1117 // if the segment is encrypted, request the key
1080 this.segmentXhr_ = videojs.Hls.xhr({ 1118 if (segment.key) {
1081 uri: segmentInfo.uri, 1119 this.fetchKey_(segment);
1082 responseType: 'arraybuffer',
1083 withCredentials: this.source_.withCredentials,
1084 // Set xhr timeout to 150% of the segment duration to allow us
1085 // some time to switch renditions in the event of a catastrophic
1086 // decrease in network performance or a server issue.
1087 timeout: (segment.duration * 1.5) * 1000,
1088 headers: this.segmentXhrHeaders_(segment)
1089 }, function(error, request) {
1090 // This is a timeout of a previously aborted segment request
1091 // so simply ignore it
1092 if (!self.segmentXhr_ || request !== self.segmentXhr_) {
1093 return;
1094 } 1120 }
1095 1121
1096 // the segment request is no longer outstanding 1122 // request the next segment
1097 self.segmentXhr_ = null; 1123 this.segmentXhr_ = Hls.xhr({
1098 1124 uri: segmentInfo.uri,
1099 // if a segment request times out, we may have better luck with another playlist 1125 responseType: 'arraybuffer',
1100 if (request.timedout) { 1126 withCredentials: this.source_.withCredentials,
1101 self.bandwidth = 1; 1127 // Set xhr timeout to 150% of the segment duration to allow us
1102 return self.playlists.media(self.selectPlaylist()); 1128 // some time to switch renditions in the event of a catastrophic
1103 } 1129 // decrease in network performance or a server issue.
1130 timeout: (segment.duration * 1.5) * 1000,
1131 headers: this.segmentXhrHeaders_(segment)
1132 }, (error, request) => {
1133 // This is a timeout of a previously aborted segment request
1134 // so simply ignore it
1135 if (!this.segmentXhr_ || request !== this.segmentXhr_) {
1136 return;
1137 }
1104 1138
1105 // otherwise, trigger a network error 1139 // the segment request is no longer outstanding
1106 if (!request.aborted && error) { 1140 this.segmentXhr_ = null;
1107 return self.blacklistCurrentPlaylist_({
1108 status: request.status,
1109 message: 'HLS segment request error at URL: ' + segmentInfo.uri,
1110 code: (request.status >= 500) ? 4 : 2
1111 });
1112 }
1113 1141
1114 // stop processing if the request was aborted 1142 // if a segment request times out, we may have better luck with another playlist
1115 if (!request.response) { 1143 if (request.timedout) {
1116 return; 1144 this.bandwidth = 1;
1117 } 1145 return this.playlists.media(this.selectPlaylist());
1146 }
1118 1147
1119 self.lastSegmentLoaded_ = segment; 1148 // otherwise, trigger a network error
1120 self.setBandwidth(request); 1149 if (!request.aborted && error) {
1150 return this.blacklistCurrentPlaylist_({
1151 status: request.status,
1152 message: 'HLS segment request error at URL: ' + segmentInfo.uri,
1153 code: (request.status >= 500) ? 4 : 2
1154 });
1155 }
1121 1156
1122 if (segment.key) { 1157 // stop processing if the request was aborted
1123 segmentInfo.encryptedBytes = new Uint8Array(request.response); 1158 if (!request.response) {
1124 } else { 1159 return;
1125 segmentInfo.bytes = new Uint8Array(request.response); 1160 }
1126 }
1127 1161
1128 self.pendingSegment_ = segmentInfo; 1162 this.lastSegmentLoaded_ = segment;
1163 this.setBandwidth(request);
1129 1164
1130 self.tech_.trigger('progress'); 1165 if (segment.key) {
1131 self.drainBuffer(); 1166 segmentInfo.encryptedBytes = new Uint8Array(request.response);
1167 } else {
1168 segmentInfo.bytes = new Uint8Array(request.response);
1169 }
1132 1170
1133 // figure out what stream the next segment should be downloaded from 1171 this.pendingSegment_ = segmentInfo;
1134 // with the updated bandwidth information
1135 self.playlists.media(self.selectPlaylist());
1136 });
1137 1172
1138 }; 1173 this.tech_.trigger('progress');
1174 this.drainBuffer();
1139 1175
1140 videojs.HlsHandler.prototype.drainBuffer = function() { 1176 // figure out what stream the next segment should be downloaded from
1141 var 1177 // with the updated bandwidth information
1142 segmentInfo, 1178 this.playlists.media(this.selectPlaylist());
1143 mediaIndex, 1179 });
1144 playlist,
1145 offset,
1146 bytes,
1147 segment,
1148 decrypter,
1149 segIv;
1150
1151 // if the buffer is empty or the source buffer hasn't been created
1152 // yet, do nothing
1153 if (!this.pendingSegment_ || !this.sourceBuffer) {
1154 return;
1155 }
1156 1180
1157 // the pending segment has already been appended and we're waiting
1158 // for updateend to fire
1159 if (this.pendingSegment_.buffered) {
1160 return;
1161 } 1181 }
1162 1182
1163 // we can't append more data if the source buffer is busy processing 1183 drainBuffer() {
1164 // what we've already sent 1184 let segmentInfo;
1165 if (this.sourceBuffer.updating) { 1185 let mediaIndex;
1166 return; 1186 let playlist;
1167 } 1187 let bytes;
1188 let segment;
1189 let decrypter;
1190 let segIv;
1168 1191
1169 segmentInfo = this.pendingSegment_; 1192 // if the buffer is empty or the source buffer hasn't been created
1170 mediaIndex = segmentInfo.mediaIndex; 1193 // yet, do nothing
1171 playlist = segmentInfo.playlist; 1194 if (!this.pendingSegment_ || !this.sourceBuffer) {
1172 offset = segmentInfo.offset; 1195 return;
1173 bytes = segmentInfo.bytes; 1196 }
1174 segment = playlist.segments[mediaIndex];
1175
1176 if (segment.key && !bytes) {
1177 // this is an encrypted segment
1178 // if the key download failed, we want to skip this segment
1179 // but if the key hasn't downloaded yet, we want to try again later
1180 if (keyFailed(segment.key)) {
1181 return this.blacklistCurrentPlaylist_({
1182 message: 'HLS segment key request error.',
1183 code: 4
1184 });
1185 } else if (!segment.key.bytes) {
1186 1197
1187 // waiting for the key bytes, try again later 1198 // the pending segment has already been appended and we're waiting
1199 // for updateend to fire
1200 if (this.pendingSegment_.buffered) {
1188 return; 1201 return;
1189 } else if (segmentInfo.decrypter) { 1202 }
1190 1203
1191 // decryption is in progress, try again later 1204 // we can't append more data if the source buffer is busy processing
1205 // what we've already sent
1206 if (this.sourceBuffer.updating) {
1192 return; 1207 return;
1193 } else { 1208 }
1194 1209
1210 segmentInfo = this.pendingSegment_;
1211 mediaIndex = segmentInfo.mediaIndex;
1212 playlist = segmentInfo.playlist;
1213 bytes = segmentInfo.bytes;
1214 segment = playlist.segments[mediaIndex];
1215
1216 if (segment.key && !bytes) {
1217 // this is an encrypted segment
1218 // if the key download failed, we want to skip this segment
1219 // but if the key hasn't downloaded yet, we want to try again later
1220 if (keyFailed(segment.key)) {
1221 return this.blacklistCurrentPlaylist_({
1222 message: 'HLS segment key request error.',
1223 code: 4
1224 });
1225 } else if (!segment.key.bytes) {
1226 // waiting for the key bytes, try again later
1227 return;
1228 } else if (segmentInfo.decrypter) {
1229 // decryption is in progress, try again later
1230 return;
1231 }
1195 // if the media sequence is greater than 2^32, the IV will be incorrect 1232 // if the media sequence is greater than 2^32, the IV will be incorrect
1196 // assuming 10s segments, that would be about 1300 years 1233 // assuming 10s segments, that would be about 1300 years
1197 segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); 1234 segIv = segment.key.iv ||
1235 new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
1198 1236
1199 // create a decrypter to incrementally decrypt the segment 1237 // create a decrypter to incrementally decrypt the segment
1200 decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes, 1238 decrypter = new Hls.Decrypter(segmentInfo.encryptedBytes,
1201 segment.key.bytes, 1239 segment.key.bytes,
1202 segIv, 1240 segIv,
1203 function(err, bytes) { 1241 function(error, localBytes) {
1204 segmentInfo.bytes = bytes; 1242 if (error) {
1205 }); 1243 videojs.log.warn(error);
1244 }
1245 segmentInfo.bytes = localBytes;
1246 });
1206 segmentInfo.decrypter = decrypter; 1247 segmentInfo.decrypter = decrypter;
1207 return; 1248 return;
1208 } 1249 }
1209 }
1210 1250
1211 this.pendingSegment_.buffered = this.tech_.buffered(); 1251 this.pendingSegment_.buffered = this.tech_.buffered();
1252
1253 if (segmentInfo.timestampOffset !== null) {
1254 this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset;
1255 }
1212 1256
1213 if (segmentInfo.timestampOffset !== null) { 1257 // the segment is asynchronously added to the current buffered data
1214 this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset; 1258 this.sourceBuffer.appendBuffer(bytes);
1215 } 1259 }
1216 1260
1217 // the segment is asynchronously added to the current buffered data 1261 updateEndHandler_() {
1218 this.sourceBuffer.appendBuffer(bytes); 1262 let segmentInfo = this.pendingSegment_;
1219 }; 1263 let segment;
1264 let segments;
1265 let playlist;
1266 let currentMediaIndex;
1267 let currentBuffered;
1268 let seekable;
1269 let timelineUpdate;
1220 1270
1221 videojs.HlsHandler.prototype.updateEndHandler_ = function () { 1271 this.pendingSegment_ = null;
1222 var 1272
1223 segmentInfo = this.pendingSegment_, 1273 // stop here if the update errored or was aborted
1224 segment, 1274 if (!segmentInfo) {
1225 segments, 1275 return;
1226 playlist, 1276 }
1227 currentMediaIndex,
1228 currentBuffered,
1229 seekable,
1230 timelineUpdate;
1231
1232 this.pendingSegment_ = null;
1233
1234 // stop here if the update errored or was aborted
1235 if (!segmentInfo) {
1236 return;
1237 }
1238 1277
1239 playlist = this.playlists.media(); 1278 playlist = this.playlists.media();
1240 segments = playlist.segments; 1279 segments = playlist.segments;
1241 currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence); 1280 currentMediaIndex = segmentInfo.mediaIndex +
1242 currentBuffered = this.findBufferedRange_(); 1281 (segmentInfo.mediaSequence - playlist.mediaSequence);
1282 currentBuffered = this.findBufferedRange_();
1243 1283
1244 // if we switched renditions don't try to add segment timeline 1284 // if we switched renditions don't try to add segment timeline
1245 // information to the playlist 1285 // information to the playlist
1246 if (segmentInfo.playlist.uri !== this.playlists.media().uri) { 1286 if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
1247 return this.fillBuffer(); 1287 return this.fillBuffer();
1248 } 1288 }
1249 1289
1250 // annotate the segment with any start and end time information 1290 // annotate the segment with any start and end time information
1251 // added by the media processing 1291 // added by the media processing
1252 segment = playlist.segments[currentMediaIndex]; 1292 segment = playlist.segments[currentMediaIndex];
1253 1293
1254 // when seeking to the beginning of the seekable range, it's 1294 // when seeking to the beginning of the seekable range, it's
1255 // possible that imprecise timing information may cause the seek to 1295 // possible that imprecise timing information may cause the seek to
1256 // end up earlier than the start of the range 1296 // end up earlier than the start of the range
1257 // in that case, seek again 1297 // in that case, seek again
1258 seekable = this.seekable(); 1298 seekable = this.seekable();
1259 if (this.tech_.seeking() && 1299 if (this.tech_.seeking() &&
1260 currentBuffered.length === 0) { 1300 currentBuffered.length === 0) {
1261 if (seekable.length && 1301 if (seekable.length &&
1262 this.tech_.currentTime() < seekable.start(0)) { 1302 this.tech_.currentTime() < seekable.start(0)) {
1263 var next = this.findNextBufferedRange_(); 1303 let next = this.findNextBufferedRange_();
1264 if (next.length) { 1304
1265 videojs.log('tried seeking to', this.tech_.currentTime(), 'but that was too early, retrying at', next.start(0)); 1305 if (next.length) {
1266 this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR); 1306 videojs.log('tried seeking to', this.tech_.currentTime(),
1307 'but that was too early, retrying at', next.start(0));
1308 this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR);
1309 }
1267 } 1310 }
1268 } 1311 }
1269 }
1270 1312
1313 timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
1314 this.tech_.buffered());
1271 1315
1272 timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, 1316 if (timelineUpdate && segment) {
1273 this.tech_.buffered()); 1317 segment.end = timelineUpdate;
1318 }
1274 1319
1275 if (timelineUpdate && segment) { 1320 // if we've buffered to the end of the video, let the MediaSource know
1276 segment.end = timelineUpdate; 1321 if (this.playlists.media().endList &&
1277 } 1322 currentBuffered.length &&
1323 segments[segments.length - 1].end <= currentBuffered.end(0) &&
1324 this.mediaSource.readyState === 'open') {
1325 this.mediaSource.endOfStream();
1326 return;
1327 }
1278 1328
1279 // if we've buffered to the end of the video, let the MediaSource know 1329 if (timelineUpdate !== null ||
1280 if (this.playlists.media().endList && 1330 segmentInfo.buffered.length !== this.tech_.buffered().length) {
1281 currentBuffered.length && 1331 this.updateDuration(playlist);
1282 segments[segments.length - 1].end <= currentBuffered.end(0) && 1332 // check if it's time to download the next segment
1283 this.mediaSource.readyState === 'open') { 1333 this.fillBuffer();
1284 this.mediaSource.endOfStream(); 1334 return;
1285 return; 1335 }
1286 }
1287 1336
1288 if (timelineUpdate !== null || 1337 // the last segment append must have been entirely in the
1289 segmentInfo.buffered.length !== this.tech_.buffered().length) { 1338 // already buffered time ranges. just buffer forward until we
1290 this.updateDuration(playlist); 1339 // find a segment that adds to the buffered time ranges and
1291 // check if it's time to download the next segment 1340 // improves subsequent media index calculations.
1292 this.fillBuffer(); 1341 this.fillBuffer(currentMediaIndex + 1);
1293 return; 1342 return;
1294 } 1343 }
1295 1344
1296 // the last segment append must have been entirely in the 1345 /**
1297 // already buffered time ranges. just buffer forward until we 1346 * Attempt to retrieve the key for a particular media segment.
1298 // find a segment that adds to the buffered time ranges and 1347 */
1299 // improves subsequent media index calculations. 1348 fetchKey_(segment) {
1300 this.fillBuffer(currentMediaIndex + 1); 1349 let key;
1301 return; 1350 let settings;
1302 }; 1351 let receiveKey;
1303 1352
1304 /** 1353 // if there is a pending XHR or no segments, don't do anything
1305 * Attempt to retrieve the key for a particular media segment. 1354 if (this.keyXhr_) {
1306 */ 1355 return;
1307 videojs.HlsHandler.prototype.fetchKey_ = function(segment) { 1356 }
1308 var key, self, settings, receiveKey;
1309 1357
1310 // if there is a pending XHR or no segments, don't do anything 1358 settings = this.options_;
1311 if (this.keyXhr_) {
1312 return;
1313 }
1314 1359
1315 self = this; 1360 /**
1316 settings = this.options_; 1361 * Handle a key XHR response.
1362 */
1363 receiveKey = (keyRecieved) => {
1364 return (error, request) => {
1365 let view;
1317 1366
1318 /** 1367 this.keyXhr_ = null;
1319 * Handle a key XHR response.
1320 */
1321 receiveKey = function(key) {
1322 return function(error, request) {
1323 var view;
1324 self.keyXhr_ = null;
1325
1326 if (error || !request.response || request.response.byteLength !== 16) {
1327 key.retries = key.retries || 0;
1328 key.retries++;
1329 if (!request.aborted) {
1330 // try fetching again
1331 self.fetchKey_(segment);
1332 }
1333 return;
1334 }
1335 1368
1336 view = new DataView(request.response); 1369 if (error || !request.response || request.response.byteLength !== 16) {
1337 key.bytes = new Uint32Array([ 1370 keyRecieved.retries = keyRecieved.retries || 0;
1338 view.getUint32(0), 1371 keyRecieved.retries++;
1339 view.getUint32(4), 1372 if (!request.aborted) {
1340 view.getUint32(8), 1373 // try fetching again
1341 view.getUint32(12) 1374 this.fetchKey_(segment);
1342 ]); 1375 }
1376 return;
1377 }
1343 1378
1344 // check to see if this allows us to make progress buffering now 1379 view = new DataView(request.response);
1345 self.checkBuffer_(); 1380 keyRecieved.bytes = new Uint32Array([
1381 view.getUint32(0),
1382 view.getUint32(4),
1383 view.getUint32(8),
1384 view.getUint32(12)
1385 ]);
1386
1387 // check to see if this allows us to make progress buffering now
1388 this.checkBuffer_();
1389 };
1346 }; 1390 };
1347 };
1348 1391
1349 key = segment.key; 1392 key = segment.key;
1350 1393
1351 // nothing to do if this segment is unencrypted 1394 // nothing to do if this segment is unencrypted
1352 if (!key) { 1395 if (!key) {
1353 return; 1396 return;
1354 } 1397 }
1355 1398
1356 // request the key if the retry limit hasn't been reached 1399 // request the key if the retry limit hasn't been reached
1357 if (!key.bytes && !keyFailed(key)) { 1400 if (!key.bytes && !keyFailed(key)) {
1358 this.keyXhr_ = videojs.Hls.xhr({ 1401 this.keyXhr_ = Hls.xhr({
1359 uri: this.playlistUriToUrl(key.uri), 1402 uri: this.playlistUriToUrl(key.uri),
1360 responseType: 'arraybuffer', 1403 responseType: 'arraybuffer',
1361 withCredentials: settings.withCredentials 1404 withCredentials: settings.withCredentials
1362 }, receiveKey(key)); 1405 }, receiveKey(key));
1363 return; 1406 return;
1407 }
1364 } 1408 }
1365 }; 1409 }
1366 1410
1367 /** 1411 /**
1368 * Whether the browser has built-in HLS support. 1412 * Attempts to find the buffered TimeRange that contains the specified
1413 * time, or where playback is currently happening if no specific time
1414 * is specified.
1415 * @param time (optional) {number} the time to filter on. Defaults to
1416 * currentTime.
1417 * @return a new TimeRanges object.
1369 */ 1418 */
1370 videojs.Hls.supportsNativeHls = (function() { 1419 HlsHandler.prototype.findBufferedRange_ =
1371 var 1420 filterBufferedRanges(function(start, end, time) {
1372 video = document.createElement('video'), 1421 return start - TIME_FUDGE_FACTOR <= time &&
1373 xMpegUrl, 1422 end + TIME_FUDGE_FACTOR >= time;
1374 vndMpeg; 1423 });
1375
1376 // native HLS is definitely not supported if HTML5 video isn't
1377 if (!videojs.getComponent('Html5').isSupported()) {
1378 return false;
1379 }
1380
1381 xMpegUrl = video.canPlayType('application/x-mpegURL');
1382 vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
1383 return (/probably|maybe/).test(xMpegUrl) ||
1384 (/probably|maybe/).test(vndMpeg);
1385 })();
1386
1387 // HLS is a source handler, not a tech. Make sure attempts to use it
1388 // as one do not cause exceptions.
1389 videojs.Hls.isSupported = function() {
1390 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
1391 'your player\'s techOrder.');
1392 };
1393
1394 /** 1424 /**
1395 * A comparator function to sort two playlist object by bandwidth. 1425 * Returns the TimeRanges that begin at or later than the specified
1396 * @param left {object} a media playlist object 1426 * time.
1397 * @param right {object} a media playlist object 1427 * @param time (optional) {number} the time to filter on. Defaults to
1398 * @return {number} Greater than zero if the bandwidth attribute of 1428 * currentTime.
1399 * left is greater than the corresponding attribute of right. Less 1429 * @return a new TimeRanges object.
1400 * than zero if the bandwidth of right is greater than left and
1401 * exactly zero if the two are equal.
1402 */ 1430 */
1403 videojs.Hls.comparePlaylistBandwidth = function(left, right) { 1431 HlsHandler.prototype.findNextBufferedRange_ =
1404 var leftBandwidth, rightBandwidth; 1432 filterBufferedRanges(function(start, end, time) {
1405 if (left.attributes && left.attributes.BANDWIDTH) { 1433 return start - TIME_FUDGE_FACTOR >= time;
1406 leftBandwidth = left.attributes.BANDWIDTH; 1434 });
1407 }
1408 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
1409 if (right.attributes && right.attributes.BANDWIDTH) {
1410 rightBandwidth = right.attributes.BANDWIDTH;
1411 }
1412 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
1413
1414 return leftBandwidth - rightBandwidth;
1415 };
1416 1435
1417 /** 1436 /**
1418 * A comparator function to sort two playlist object by resolution (width). 1437 * The Source Handler object, which informs video.js what additional
1419 * @param left {object} a media playlist object 1438 * MIME types are supported and sets up playback. It is registered
1420 * @param right {object} a media playlist object 1439 * automatically to the appropriate tech based on the capabilities of
1421 * @return {number} Greater than zero if the resolution.width attribute of 1440 * the browser it is running in. It is not necessary to use or modify
1422 * left is greater than the corresponding attribute of right. Less 1441 * this object in normal usage.
1423 * than zero if the resolution.width of right is greater than left and
1424 * exactly zero if the two are equal.
1425 */ 1442 */
1426 videojs.Hls.comparePlaylistResolution = function(left, right) { 1443 const HlsSourceHandler = function(mode) {
1427 var leftWidth, rightWidth; 1444 return {
1428 1445 canHandleSource(srcObj) {
1429 if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { 1446 return HlsSourceHandler.canPlayType(srcObj.type);
1430 leftWidth = left.attributes.RESOLUTION.width; 1447 },
1431 } 1448 handleSource(source, tech) {
1432 1449 if (mode === 'flash') {
1433 leftWidth = leftWidth || window.Number.MAX_VALUE; 1450 // We need to trigger this asynchronously to give others the chance
1434 1451 // to bind to the event when a source is set at player creation
1435 if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 1452 tech.setTimeout(function() {
1436 rightWidth = right.attributes.RESOLUTION.width; 1453 tech.trigger('loadstart');
1437 } 1454 }, 1);
1455 }
1456 tech.hls = new HlsHandler(tech, {
1457 source,
1458 mode
1459 });
1460 tech.hls.src(source.src);
1461 return tech.hls;
1462 },
1463 canPlayType(type) {
1464 return HlsSourceHandler.canPlayType(type);
1465 }
1466 };
1467 };
1438 1468
1439 rightWidth = rightWidth || window.Number.MAX_VALUE; 1469 HlsSourceHandler.canPlayType = function(type) {
1470 let mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
1440 1471
1441 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 1472 // favor native HLS support if it's available
1442 // have the same media dimensions/ resolution 1473 if (Hls.supportsNativeHls) {
1443 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 1474 return false;
1444 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
1445 } else {
1446 return leftWidth - rightWidth;
1447 } 1475 }
1476 return mpegurlRE.test(type);
1448 }; 1477 };
1449 1478
1450 /** 1479 if (typeof videojs.MediaSource === 'undefined' ||
1451 * Constructs a new URI by interpreting a path relative to another 1480 typeof videojs.URL === 'undefined') {
1452 * URI. 1481 videojs.MediaSource = MediaSource;
1453 * @param basePath {string} a relative or absolute URI 1482 videojs.URL = URL;
1454 * @param path {string} a path part to combine with the base 1483 }
1455 * @return {string} a URI that is equivalent to composing `base`
1456 * with `path`
1457 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
1458 */
1459 resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) {
1460 // use the base element to get the browser to handle URI resolution
1461 var
1462 oldBase = document.querySelector('base'),
1463 docHead = document.querySelector('head'),
1464 a = document.createElement('a'),
1465 base = oldBase,
1466 oldHref,
1467 result;
1468
1469 // prep the document
1470 if (oldBase) {
1471 oldHref = oldBase.href;
1472 } else {
1473 base = docHead.appendChild(document.createElement('base'));
1474 }
1475 1484
1476 base.href = basePath; 1485 // register source handlers with the appropriate techs
1477 a.href = path; 1486 if (MediaSource.supportsNativeMediaSources()) {
1478 result = a.href; 1487 videojs.getComponent('Html5').registerSourceHandler(HlsSourceHandler('html5'));
1488 }
1489 if (window.Uint8Array) {
1490 videojs.getComponent('Flash').registerSourceHandler(HlsSourceHandler('flash'));
1491 }
1479 1492
1480 // clean up 1493 videojs.HlsHandler = HlsHandler;
1481 if (oldBase) { 1494 videojs.HlsSourceHandler = HlsSourceHandler;
1482 oldBase.href = oldHref; 1495 videojs.Hls = Hls;
1483 } else { 1496 videojs.m3u8 = m3u8;
1484 docHead.removeChild(base);
1485 }
1486 return result;
1487 };
1488 1497
1489 })(window, window.videojs, document); 1498 export default {
1499 Hls,
1500 HlsHandler,
1501 HlsSourceHandler
1502 };
......
...@@ -90,7 +90,6 @@ function() { ...@@ -90,7 +90,6 @@ function() {
90 90
91 }); 91 });
92 92
93
94 QUnit.module('Incremental Processing', { 93 QUnit.module('Incremental Processing', {
95 beforeEach() { 94 beforeEach() {
96 this.clock = sinon.useFakeTimers(); 95 this.clock = sinon.useFakeTimers();
......
...@@ -13,13 +13,6 @@ ...@@ -13,13 +13,6 @@
13 <script src="/node_modules/sinon/pkg/sinon.js"></script> 13 <script src="/node_modules/sinon/pkg/sinon.js"></script>
14 <script src="/node_modules/qunitjs/qunit/qunit.js"></script> 14 <script src="/node_modules/qunitjs/qunit/qunit.js"></script>
15 <script src="/node_modules/video.js/dist/video.js"></script> 15 <script src="/node_modules/video.js/dist/video.js"></script>
16 <script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
17
18 <script src="/src/videojs-contrib-hls.js"></script>
19 <script src="/dist/videojs-contrib-hls.js"></script>
20 <script src="/src/bin-utils.js"></script>
21
22 <script src="/test/videojs-contrib-hls.test.js"></script>
23 <script src="/dist-test/videojs-contrib-hls.js"></script> 16 <script src="/dist-test/videojs-contrib-hls.js"></script>
24 17
25 </body> 18 </body>
......
...@@ -11,29 +11,11 @@ var DEFAULTS = { ...@@ -11,29 +11,11 @@ var DEFAULTS = {
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 13
14 // REMOVE ME WHEN BROWSERIFIED 14 'test/**/*.test.js'
15 'node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
16
17 // these two stub old functionality
18 'src/videojs-contrib-hls.js',
19 'dist/videojs-contrib-hls.js',
20
21 'src/bin-utils.js',
22
23 'test/stub.test.js',
24
25 'test/videojs-contrib-hls.test.js',
26 'test/m3u8.test.js',
27 'test/playlist.test.js',
28 'test/playlist-loader.test.js',
29 'test/decrypter.test.js',
30 // END REMOVE ME
31 // 'test/**/*.js'
32 ], 15 ],
33 16
34 exclude: [ 17 exclude: [
35 'test/bundle.js', 18 'test/data/**'
36 // 'test/data/**'
37 ], 19 ],
38 20
39 plugins: [ 21 plugins: [
...@@ -42,7 +24,7 @@ var DEFAULTS = { ...@@ -42,7 +24,7 @@ var DEFAULTS = {
42 ], 24 ],
43 25
44 preprocessors: { 26 preprocessors: {
45 'test/{playlist*,decrypter,stub,m3u8}.test.js': ['browserify'] 27 'test/**/*.test.js': ['browserify']
46 }, 28 },
47 29
48 reporters: ['dots'], 30 reporters: ['dots'],
......
...@@ -29,9 +29,11 @@ QUnit.test('the environment is sane', function(assert) { ...@@ -29,9 +29,11 @@ QUnit.test('the environment is sane', function(assert) {
29 assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); 29 assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists');
30 assert.strictEqual(typeof sinon, 'object', 'sinon exists'); 30 assert.strictEqual(typeof sinon, 'object', 'sinon exists');
31 assert.strictEqual(typeof videojs, 'function', 'videojs exists'); 31 assert.strictEqual(typeof videojs, 'function', 'videojs exists');
32 assert.strictEqual(typeof videojs.MediaSource, 'object', 'MediaSource is an object'); 32 assert.strictEqual(typeof videojs.MediaSource, 'function', 'MediaSource is an object');
33 assert.strictEqual(typeof videojs.URL, 'object', 'URL is an object'); 33 assert.strictEqual(typeof videojs.URL, 'object', 'URL is an object');
34 assert.strictEqual(typeof videojs.Hls, 'object', 'Hls is an object'); 34 assert.strictEqual(typeof videojs.Hls, 'object', 'Hls is an object');
35 assert.strictEqual(typeof videojs.HlsSourceHandler,'function', 'HlsSourceHandler is a function'); 35 assert.strictEqual(typeof videojs.HlsSourceHandler,
36 'function',
37 'HlsSourceHandler is a function');
36 assert.strictEqual(typeof videojs.HlsHandler, 'function', 'HlsHandler is a function'); 38 assert.strictEqual(typeof videojs.HlsHandler, 'function', 'HlsHandler is a function');
37 }); 39 });
......
1 import manifests from './test-manifests';
2 import expected from './test-expected';
3 window.manifests = manifests;
4 window.expected = expected;
5
This diff could not be displayed because it is too large.