99580d5c by brandonocasey

browserify-p5: videojs-contrib-hls and bin-utils conversion

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