d15c2a24 by Brandon Casey

Hls.GOAL_BUFFER_LENGTH can override GOAL_BUFFER_LENGTH again (#686)

* add tests for all options in HLS
* add configuration hierarchy testing
* Hls.GOAL_BUFFER_LENGTH is settable to a number greater than 0 with a warning
* use Hls.GOAL_BUFFER_LENGTH if it exists
* added unit tests to verify warning logs
* fixed comment about Mbps vs MB/s
1 parent 4be578c0
1 export default {
2 GOAL_BUFFER_LENGTH: 30
3 };
...@@ -6,13 +6,11 @@ import {getMediaIndexForTime_ as getMediaIndexForTime, duration} from './playlis ...@@ -6,13 +6,11 @@ import {getMediaIndexForTime_ as getMediaIndexForTime, duration} from './playlis
6 import videojs from 'video.js'; 6 import videojs from 'video.js';
7 import SourceUpdater from './source-updater'; 7 import SourceUpdater from './source-updater';
8 import {Decrypter} from './decrypter'; 8 import {Decrypter} from './decrypter';
9 import Config from './config';
9 10
10 // in ms 11 // in ms
11 const CHECK_BUFFER_DELAY = 500; 12 const CHECK_BUFFER_DELAY = 500;
12 13
13 // the desired length of video to maintain in the buffer, in seconds
14 export const GOAL_BUFFER_LENGTH = 30;
15
16 /** 14 /**
17 * Updates segment with information about its end-point in time and, optionally, 15 * Updates segment with information about its end-point in time and, optionally,
18 * the segment duration if we have enough information to determine a segment duration 16 * the segment duration if we have enough information to determine a segment duration
...@@ -78,8 +76,9 @@ const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentB ...@@ -78,8 +76,9 @@ const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentB
78 (appendedLastSegment || bufferedToEnd); 76 (appendedLastSegment || bufferedToEnd);
79 }; 77 };
80 78
81 /* Turns segment byterange into a string suitable for use in 79 /**
82 * HTTP Range requests 80 * Turns segment byterange into a string suitable for use in
81 * HTTP Range requests
83 */ 82 */
84 const byterangeStr = function(byterange) { 83 const byterangeStr = function(byterange) {
85 let byterangeStart; 84 let byterangeStart;
...@@ -92,7 +91,8 @@ const byterangeStr = function(byterange) { ...@@ -92,7 +91,8 @@ const byterangeStr = function(byterange) {
92 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 91 return 'bytes=' + byterangeStart + '-' + byterangeEnd;
93 }; 92 };
94 93
95 /* Defines headers for use in the xhr request for a particular segment. 94 /**
95 * Defines headers for use in the xhr request for a particular segment.
96 */ 96 */
97 const segmentXhrHeaders = function(segment) { 97 const segmentXhrHeaders = function(segment) {
98 let headers = {}; 98 let headers = {};
...@@ -385,7 +385,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -385,7 +385,7 @@ export default class SegmentLoader extends videojs.EventTarget {
385 385
386 // if there is plenty of content buffered, and the video has 386 // if there is plenty of content buffered, and the video has
387 // been played before relax for awhile 387 // been played before relax for awhile
388 if (this.hasPlayed_() && bufferedTime >= GOAL_BUFFER_LENGTH) { 388 if (this.hasPlayed_() && bufferedTime >= Config.GOAL_BUFFER_LENGTH) {
389 return null; 389 return null;
390 } 390 }
391 mediaIndex = getMediaIndexForTime(playlist, 391 mediaIndex = getMediaIndexForTime(playlist,
......
...@@ -14,6 +14,7 @@ import {MediaSource, URL} from 'videojs-contrib-media-sources'; ...@@ -14,6 +14,7 @@ import {MediaSource, URL} from 'videojs-contrib-media-sources';
14 import m3u8 from './m3u8'; 14 import m3u8 from './m3u8';
15 import videojs from 'video.js'; 15 import videojs from 'video.js';
16 import MasterPlaylistController from './master-playlist-controller'; 16 import MasterPlaylistController from './master-playlist-controller';
17 import Config from './config';
17 18
18 /** 19 /**
19 * determine if an object a is differnt from 20 * determine if an object a is differnt from
...@@ -52,8 +53,23 @@ const Hls = { ...@@ -52,8 +53,23 @@ const Hls = {
52 xhr: xhrFactory() 53 xhr: xhrFactory()
53 }; 54 };
54 55
55 // the desired length of video to maintain in the buffer, in seconds 56 Object.defineProperty(Hls, 'GOAL_BUFFER_LENGTH', {
56 Hls.GOAL_BUFFER_LENGTH = 30; 57 get() {
58 videojs.log.warn('using Hls.GOAL_BUFFER_LENGTH is UNSAFE be sure ' +
59 'you know what you are doing');
60 return Config.GOAL_BUFFER_LENGTH;
61 },
62 set(v) {
63 videojs.log.warn('using Hls.GOAL_BUFFER_LENGTH is UNSAFE be sure ' +
64 'you know what you are doing');
65 if (typeof v !== 'number' || v <= 0) {
66 videojs.log.warn('value passed to Hls.GOAL_BUFFER_LENGTH ' +
67 'must be a number and greater than 0');
68 return;
69 }
70 Config.GOAL_BUFFER_LENGTH = v;
71 }
72 });
57 73
58 // A fudge factor to apply to advertised playlist bitrates to account for 74 // A fudge factor to apply to advertised playlist bitrates to account for
59 // temporary flucations in client bandwidth 75 // temporary flucations in client bandwidth
...@@ -283,14 +299,13 @@ class HlsHandler extends Component { ...@@ -283,14 +299,13 @@ class HlsHandler extends Component {
283 } 299 }
284 } 300 }
285 301
286 this.options_ = videojs.mergeOptions(videojs.options.hls || {}, options.hls);
287 this.tech_ = tech; 302 this.tech_ = tech;
288 this.source_ = source; 303 this.source_ = source;
289 304
290 // start playlist selection at a reasonable bandwidth for 305 // handle global & Source Handler level options
291 // broadband internet 306 this.options_ = videojs.mergeOptions(videojs.options.hls || {}, options.hls);
292 // 0.5 Mbps 307 this.setOptions_();
293 this.bandwidth = this.options_.bandwidth || 4194304; 308
294 this.bytesReceived = 0; 309 this.bytesReceived = 0;
295 310
296 // listen for fullscreenchange events for this player so that we 311 // listen for fullscreenchange events for this player so that we
...@@ -325,6 +340,24 @@ class HlsHandler extends Component { ...@@ -325,6 +340,24 @@ class HlsHandler extends Component {
325 this.on(this.tech_, 'play', this.play); 340 this.on(this.tech_, 'play', this.play);
326 } 341 }
327 342
343 setOptions_() {
344 // defaults
345 this.options_.withCredentials = this.options_.withCredentials || false;
346
347 // start playlist selection at a reasonable bandwidth for
348 // broadband internet
349 // 0.5 MB/s
350 this.options_.bandwidth = this.options_.bandwidth || 4194304;
351
352 // grab options passed to player.src
353 ['withCredentials', 'bandwidth'].forEach((option) => {
354 if (typeof this.source_[option] !== 'undefined') {
355 this.options_[option] = this.source_[option];
356 }
357 });
358
359 this.bandwidth = this.options_.bandwidth;
360 }
328 /** 361 /**
329 * called when player.src gets called, handle a new source 362 * called when player.src gets called, handle a new source
330 * 363 *
...@@ -335,17 +368,13 @@ class HlsHandler extends Component { ...@@ -335,17 +368,13 @@ class HlsHandler extends Component {
335 if (!src) { 368 if (!src) {
336 return; 369 return;
337 } 370 }
338 371 this.setOptions_();
339 ['withCredentials', 'bandwidth'].forEach((option) => { 372 // add master playlist controller options
340 if (typeof this.source_[option] !== 'undefined') {
341 this.options_[option] = this.source_[option];
342 }
343 });
344 this.options_.url = this.source_.src; 373 this.options_.url = this.source_.src;
345 this.options_.tech = this.tech_; 374 this.options_.tech = this.tech_;
346 this.options_.externHls = Hls; 375 this.options_.externHls = Hls;
347 this.options_.bandwidth = this.bandwidth;
348 this.masterPlaylistController_ = new MasterPlaylistController(this.options_); 376 this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
377
349 // `this` in selectPlaylist should be the HlsHandler for backwards 378 // `this` in selectPlaylist should be the HlsHandler for backwards
350 // compatibility with < v2 379 // compatibility with < v2
351 this.masterPlaylistController_.selectPlaylist = 380 this.masterPlaylistController_.selectPlaylist =
......
1 import QUnit from 'qunit';
2 import {
3 createPlayer,
4 useFakeEnvironment,
5 openMediaSource,
6 useFakeMediaSource
7 } from './test-helpers.js';
8 import videojs from 'video.js';
9
10 /* eslint-disable no-unused-vars */
11 // we need this so that it can register hls with videojs
12 import {HlsSourceHandler, HlsHandler, Hls} from '../src/videojs-contrib-hls';
13 /* eslint-enable no-unused-vars */
14 import Config from '../src/config';
15
16 // list of posible options
17 // name - the proprety name
18 // default - the default value
19 // test - alternative value to verify that default is not used
20 // alt - another alternative value to very that test/default are not used
21 const options = [{
22 name: 'withCredentials',
23 default: false,
24 test: true,
25 alt: false
26 }, {
27 name: 'bandwidth',
28 default: 4194304,
29 test: 5,
30 alt: 555
31 }];
32
33 QUnit.module('Configuration - Deprication', {
34 beforeEach() {
35 this.env = useFakeEnvironment();
36 this.requests = this.env.requests;
37 this.mse = useFakeMediaSource();
38 this.clock = this.env.clock;
39 this.old = {};
40 this.old.GOAL_BUFFER_LENGTH = Config.GOAL_BUFFER_LENGTH;
41 // force the HLS tech to run
42 this.old.NativeHlsSupport = videojs.Hls.supportsNativeHls;
43 videojs.Hls.supportsNativeHls = false;
44 },
45
46 afterEach() {
47 Config.GOAL_BUFFER_LENGTH = this.old.GOAL_BUFFER_LENGTH;
48
49 this.env.restore();
50 this.mse.restore();
51 videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport;
52 }
53 });
54
55 QUnit.test('GOAL_BUFFER_LENGTH get warning', function() {
56 QUnit.equal(Hls.GOAL_BUFFER_LENGTH,
57 Config.GOAL_BUFFER_LENGTH,
58 'Hls.GOAL_BUFFER_LENGTH returns the default');
59 QUnit.equal(this.env.log.warn.calls, 1, 'logged a warning');
60 });
61
62 QUnit.test('GOAL_BUFFER_LENGTH set warning', function() {
63 Hls.GOAL_BUFFER_LENGTH = 10;
64 QUnit.equal(this.env.log.warn.calls, 1, 'logged a warning');
65
66 QUnit.equal(Config.GOAL_BUFFER_LENGTH, 10, 'returns what we set it to');
67 });
68
69 QUnit.test('GOAL_BUFFER_LENGTH set warning and invalid', function() {
70 Hls.GOAL_BUFFER_LENGTH = 'nope';
71 QUnit.equal(this.env.log.warn.calls, 2, 'logged two warnings');
72
73 QUnit.equal(Config.GOAL_BUFFER_LENGTH, 30, 'default');
74
75 Hls.GOAL_BUFFER_LENGTH = 0;
76 QUnit.equal(this.env.log.warn.calls, 2, 'logged two warnings');
77
78 QUnit.equal(Config.GOAL_BUFFER_LENGTH, 30, 'default');
79 });
80
81 QUnit.module('Configuration - Options', {
82 beforeEach() {
83 this.env = useFakeEnvironment();
84 this.requests = this.env.requests;
85 this.mse = useFakeMediaSource();
86 this.clock = this.env.clock;
87 this.old = {};
88
89 // force the HLS tech to run
90 this.old.NativeHlsSupport = videojs.Hls.supportsNativeHls;
91 videojs.Hls.supportsNativeHls = false;
92 },
93
94 afterEach() {
95 this.env.restore();
96 this.mse.restore();
97 videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport;
98
99 this.player.dispose();
100 videojs.options.hls = {};
101
102 }
103 });
104
105 options.forEach((opt) => {
106 QUnit.test(`default ${opt.name}`, function() {
107 this.player = createPlayer();
108 this.player.src({
109 src: 'http://example.com/media.m3u8',
110 type: 'application/vnd.apple.mpegurl'
111 });
112
113 let hls = this.player.tech_.hls;
114
115 openMediaSource(this.player, this.clock);
116 QUnit.equal(hls.options_[opt.name],
117 opt.default,
118 `${opt.name} should be default`);
119 });
120
121 QUnit.test(`global ${opt.name}`, function() {
122 videojs.options.hls[opt.name] = opt.test;
123 this.player = createPlayer();
124 this.player.src({
125 src: 'http://example.com/media.m3u8',
126 type: 'application/vnd.apple.mpegurl'
127 });
128
129 let hls = this.player.tech_.hls;
130
131 openMediaSource(this.player, this.clock);
132 QUnit.equal(hls.options_[opt.name],
133 opt.test,
134 `${opt.name} should be equal to global`);
135 });
136
137 QUnit.test(`sourceHandler ${opt.name}`, function() {
138 let sourceHandlerOptions = {html5: {hls: {}}};
139
140 sourceHandlerOptions.html5.hls[opt.name] = opt.test;
141 this.player = createPlayer(sourceHandlerOptions);
142 this.player.src({
143 src: 'http://example.com/media.m3u8',
144 type: 'application/vnd.apple.mpegurl'
145 });
146
147 let hls = this.player.tech_.hls;
148
149 openMediaSource(this.player, this.clock);
150 QUnit.equal(hls.options_[opt.name],
151 opt.test,
152 `${opt.name} should be equal to sourceHandler Option`);
153 });
154
155 QUnit.test(`src ${opt.name}`, function() {
156 let srcOptions = {
157 src: 'http://example.com/media.m3u8',
158 type: 'application/vnd.apple.mpegurl'
159 };
160
161 srcOptions[opt.name] = opt.test;
162 this.player = createPlayer();
163 this.player.src(srcOptions);
164
165 let hls = this.player.tech_.hls;
166
167 openMediaSource(this.player, this.clock);
168 QUnit.equal(hls.options_[opt.name],
169 opt.test,
170 `${opt.name} should be equal to src option`);
171 });
172
173 QUnit.test(`srcHandler overrides global ${opt.name}`, function() {
174 let sourceHandlerOptions = {html5: {hls: {}}};
175
176 sourceHandlerOptions.html5.hls[opt.name] = opt.test;
177 videojs.options.hls[opt.name] = opt.alt;
178 this.player = createPlayer(sourceHandlerOptions);
179 this.player.src({
180 src: 'http://example.com/media.m3u8',
181 type: 'application/vnd.apple.mpegurl'
182 });
183
184 let hls = this.player.tech_.hls;
185
186 openMediaSource(this.player, this.clock);
187 QUnit.equal(hls.options_[opt.name],
188 opt.test,
189 `${opt.name} should be equal to sourchHandler option`);
190 });
191
192 QUnit.test(`src overrides sourceHandler ${opt.name}`, function() {
193 let sourceHandlerOptions = {html5: {hls: {}}};
194 let srcOptions = {
195 src: 'http://example.com/media.m3u8',
196 type: 'application/vnd.apple.mpegurl'
197 };
198
199 sourceHandlerOptions.html5.hls[opt.name] = opt.alt;
200 srcOptions[opt.name] = opt.test;
201 this.player = createPlayer(sourceHandlerOptions);
202 this.player.src(srcOptions);
203
204 let hls = this.player.tech_.hls;
205
206 openMediaSource(this.player, this.clock);
207 QUnit.equal(hls.options_[opt.name],
208 opt.test,
209 `${opt.name} should be equal to sourchHandler option`);
210 });
211 });
212
213 QUnit.module('Configuration - Global Only', {
214 beforeEach() {
215 videojs.options.hls = {};
216 },
217
218 afterEach() {
219 videojs.options.hls = {};
220 }
221 });
222
223 QUnit.test('global mode override - flash', function() {
224 videojs.options.hls.mode = 'flash';
225 let htmlSourceHandler = new HlsSourceHandler('html5');
226 let flashSourceHandler = new HlsSourceHandler('flash');
227
228 QUnit.equal(
229 htmlSourceHandler.canHandleSource({type: 'application/x-mpegURL'}),
230 false,
231 'Cannot play html as we are overriden not to');
232
233 QUnit.equal(
234 flashSourceHandler.canHandleSource({type: 'application/x-mpegURL'}),
235 true,
236 'Can play flash as it is supported and overides allow');
237 });
238
239 QUnit.test('global mode override - html', function() {
240 videojs.options.hls.mode = 'html5';
241 let htmlSourceHandler = new HlsSourceHandler('html5');
242 let flashSourceHandler = new HlsSourceHandler('flash');
243
244 QUnit.equal(
245 htmlSourceHandler.canHandleSource({type: 'application/x-mpegURL'}),
246 true,
247 'Can play html as we support it and overides allow');
248
249 QUnit.equal(
250 flashSourceHandler.canHandleSource({type: 'application/x-mpegURL'}),
251 false,
252 'Cannot play flash as we are overiden not to');
253 });
254
1 import QUnit from 'qunit'; 1 import QUnit from 'qunit';
2 import {GOAL_BUFFER_LENGTH, default as SegmentLoader} from '../src/segment-loader'; 2 import SegmentLoader from '../src/segment-loader';
3 import videojs from 'video.js'; 3 import videojs from 'video.js';
4 import xhrFactory from '../src/xhr'; 4 import xhrFactory from '../src/xhr';
5 import { useFakeEnvironment, useFakeMediaSource } from './test-helpers.js'; 5 import {useFakeEnvironment, useFakeMediaSource} from './test-helpers.js';
6 import Config from '../src/config';
6 7
7 const playlistWithDuration = function(time, conf) { 8 const playlistWithDuration = function(time, conf) {
8 let result = { 9 let result = {
...@@ -184,13 +185,13 @@ QUnit.test('regularly checks the buffer while unpaused', function() { ...@@ -184,13 +185,13 @@ QUnit.test('regularly checks the buffer while unpaused', function() {
184 this.requests[0].response = new Uint8Array(10).buffer; 185 this.requests[0].response = new Uint8Array(10).buffer;
185 this.requests.shift().respond(200, null, ''); 186 this.requests.shift().respond(200, null, '');
186 sourceBuffer.buffered = videojs.createTimeRanges([[ 187 sourceBuffer.buffered = videojs.createTimeRanges([[
187 0, GOAL_BUFFER_LENGTH 188 0, Config.GOAL_BUFFER_LENGTH
188 ]]); 189 ]]);
189 sourceBuffer.trigger('updateend'); 190 sourceBuffer.trigger('updateend');
190 QUnit.equal(this.requests.length, 0, 'no outstanding requests'); 191 QUnit.equal(this.requests.length, 0, 'no outstanding requests');
191 192
192 // play some video to drain the buffer 193 // play some video to drain the buffer
193 currentTime = GOAL_BUFFER_LENGTH; 194 currentTime = Config.GOAL_BUFFER_LENGTH;
194 this.clock.tick(10 * 1000); 195 this.clock.tick(10 * 1000);
195 QUnit.equal(this.requests.length, 1, 'requested another segment'); 196 QUnit.equal(this.requests.length, 1, 'requested another segment');
196 }); 197 });
...@@ -917,6 +918,18 @@ QUnit.test('key request timeouts reset bandwidth', function() { ...@@ -917,6 +918,18 @@ QUnit.test('key request timeouts reset bandwidth', function() {
917 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time'); 918 QUnit.ok(isNaN(loader.roundTrip), 'reset round trip time');
918 }); 919 });
919 920
921 QUnit.test('GOAL_BUFFER_LENGTH changes to 1 segment ' +
922 ' which is already buffered, no new request is formed', function() {
923 Config.GOAL_BUFFER_LENGTH = 1;
924 loader.mimeType(this.mimeType);
925 let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]),
926 playlistWithDuration(20),
927 0);
928
929 QUnit.ok(!segmentInfo, 'no request generated');
930 Config.GOAL_BUFFER_LENGTH = 30;
931 });
932
920 QUnit.module('Segment Loading Calculation', { 933 QUnit.module('Segment Loading Calculation', {
921 beforeEach() { 934 beforeEach() {
922 this.env = useFakeEnvironment(); 935 this.env = useFakeEnvironment();
...@@ -969,7 +982,7 @@ QUnit.test('does not download the next segment if the buffer is full', function( ...@@ -969,7 +982,7 @@ QUnit.test('does not download the next segment if the buffer is full', function(
969 loader.mimeType(this.mimeType); 982 loader.mimeType(this.mimeType);
970 983
971 buffered = videojs.createTimeRanges([ 984 buffered = videojs.createTimeRanges([
972 [0, 15 + GOAL_BUFFER_LENGTH] 985 [0, 15 + Config.GOAL_BUFFER_LENGTH]
973 ]); 986 ]);
974 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(30), 15); 987 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(30), 15);
975 988
...@@ -1067,7 +1080,7 @@ QUnit.test('adjusts calculations based on expired time', function() { ...@@ -1067,7 +1080,7 @@ QUnit.test('adjusts calculations based on expired time', function() {
1067 1080
1068 segmentInfo = loader.checkBuffer_(buffered, 1081 segmentInfo = loader.checkBuffer_(buffered,
1069 playlist, 1082 playlist,
1070 40 - GOAL_BUFFER_LENGTH); 1083 40 - Config.GOAL_BUFFER_LENGTH);
1071 1084
1072 QUnit.ok(segmentInfo, 'fetched a segment'); 1085 QUnit.ok(segmentInfo, 'fetched a segment');
1073 QUnit.equal(segmentInfo.uri, '2.ts', 'accounted for expired time'); 1086 QUnit.equal(segmentInfo.uri, '2.ts', 'accounted for expired time');
......
...@@ -1145,39 +1145,6 @@ QUnit.test('if withCredentials global option is used, withCredentials is set on ...@@ -1145,39 +1145,6 @@ QUnit.test('if withCredentials global option is used, withCredentials is set on
1145 videojs.options.hls = hlsOptions; 1145 videojs.options.hls = hlsOptions;
1146 }); 1146 });
1147 1147
1148 QUnit.test('if withCredentials src option is used, withCredentials is set on the XHR object', function() {
1149 this.player.dispose();
1150 this.player = createPlayer();
1151 this.player.src({
1152 src: 'http://example.com/media.m3u8',
1153 type: 'application/vnd.apple.mpegurl',
1154 withCredentials: true
1155 });
1156 openMediaSource(this.player, this.clock);
1157 QUnit.ok(this.requests[0].withCredentials,
1158 'with credentials should be set to true if that option is passed in');
1159 });
1160
1161 QUnit.test('src level credentials supersede the global options', function() {
1162 let hlsOptions = videojs.options.hls;
1163
1164 this.player.dispose();
1165 videojs.options.hls = {
1166 withCredentials: false
1167 };
1168
1169 this.player = createPlayer();
1170 this.player.src({
1171 src: 'http://example.com/media.m3u8',
1172 type: 'application/vnd.apple.mpegurl',
1173 withCredentials: true
1174 });
1175 openMediaSource(this.player, this.clock);
1176 QUnit.ok(this.requests[0].withCredentials,
1177 'with credentials should be set to true if that option is passed in');
1178 videojs.options.hls = hlsOptions;
1179 });
1180
1181 QUnit.test('if mode global option is used, mode is set to global option', function() { 1148 QUnit.test('if mode global option is used, mode is set to global option', function() {
1182 let hlsOptions = videojs.options.hls; 1149 let hlsOptions = videojs.options.hls;
1183 1150
......