86a32489 by Matthew Neil Committed by Jon-Carlos Rivera

Fix request timeouts (#770)

* Removed segment timeout when on the lowest enabled rendition
* Move timeout check to only run when needed
1 parent 8e167d54
...@@ -59,6 +59,10 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -59,6 +59,10 @@ export default class MasterPlaylistController extends videojs.EventTarget {
59 this.hls_ = tech.hls; 59 this.hls_ = tech.hls;
60 this.mode_ = mode; 60 this.mode_ = mode;
61 this.audioTracks_ = []; 61 this.audioTracks_ = [];
62 this.requestOptions_ = {
63 withCredentials: this.withCredentials,
64 timeout: null
65 };
62 66
63 this.mediaSource = new videojs.MediaSource({ mode }); 67 this.mediaSource = new videojs.MediaSource({ mode });
64 this.mediaSource.on('audioinfo', (e) => this.trigger(e)); 68 this.mediaSource.on('audioinfo', (e) => this.trigger(e));
...@@ -69,7 +73,6 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -69,7 +73,6 @@ export default class MasterPlaylistController extends videojs.EventTarget {
69 hls: this.hls_, 73 hls: this.hls_,
70 mediaSource: this.mediaSource, 74 mediaSource: this.mediaSource,
71 currentTime: this.tech_.currentTime.bind(this.tech_), 75 currentTime: this.tech_.currentTime.bind(this.tech_),
72 withCredentials: this.withCredentials,
73 seekable: () => this.seekable(), 76 seekable: () => this.seekable(),
74 seeking: () => this.tech_.seeking(), 77 seeking: () => this.tech_.seeking(),
75 setCurrentTime: (a) => this.tech_.setCurrentTime(a), 78 setCurrentTime: (a) => this.tech_.setCurrentTime(a),
...@@ -90,11 +93,14 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -90,11 +93,14 @@ export default class MasterPlaylistController extends videojs.EventTarget {
90 93
91 this.masterPlaylistLoader_.on('loadedmetadata', () => { 94 this.masterPlaylistLoader_.on('loadedmetadata', () => {
92 let media = this.masterPlaylistLoader_.media(); 95 let media = this.masterPlaylistLoader_.media();
96 let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000;
97
98 this.requestOptions_.timeout = requestTimeout;
93 99
94 // if this isn't a live video and preload permits, start 100 // if this isn't a live video and preload permits, start
95 // downloading segments 101 // downloading segments
96 if (media.endList && this.tech_.preload() !== 'none') { 102 if (media.endList && this.tech_.preload() !== 'none') {
97 this.mainSegmentLoader_.playlist(media); 103 this.mainSegmentLoader_.playlist(media, this.requestOptions_);
98 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_); 104 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_);
99 this.mainSegmentLoader_.load(); 105 this.mainSegmentLoader_.load();
100 } 106 }
...@@ -122,7 +128,7 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -122,7 +128,7 @@ export default class MasterPlaylistController extends videojs.EventTarget {
122 // that the segments have changed in some way and use that to 128 // that the segments have changed in some way and use that to
123 // update the SegmentLoader instead of doing it twice here and 129 // update the SegmentLoader instead of doing it twice here and
124 // on `mediachange` 130 // on `mediachange`
125 this.mainSegmentLoader_.playlist(updatedPlaylist); 131 this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
126 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_); 132 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_);
127 this.updateDuration(); 133 this.updateDuration();
128 134
...@@ -143,14 +149,23 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -143,14 +149,23 @@ export default class MasterPlaylistController extends videojs.EventTarget {
143 149
144 this.masterPlaylistLoader_.on('mediachange', () => { 150 this.masterPlaylistLoader_.on('mediachange', () => {
145 let media = this.masterPlaylistLoader_.media(); 151 let media = this.masterPlaylistLoader_.media();
152 let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000;
146 153
147 this.mainSegmentLoader_.abort(); 154 this.mainSegmentLoader_.abort();
148 155
156 // If we don't have any more available playlists, we don't want to
157 // timeout the request.
158 if (this.masterPlaylistLoader_.isLowestEnabledRendition_()) {
159 this.requestOptions_.timeout = 0;
160 } else {
161 this.requestOptions_.timeout = requestTimeout;
162 }
163
149 // TODO: Create a new event on the PlaylistLoader that signals 164 // TODO: Create a new event on the PlaylistLoader that signals
150 // that the segments have changed in some way and use that to 165 // that the segments have changed in some way and use that to
151 // update the SegmentLoader instead of doing it twice here and 166 // update the SegmentLoader instead of doing it twice here and
152 // on `loadedplaylist` 167 // on `loadedplaylist`
153 this.mainSegmentLoader_.playlist(media); 168 this.mainSegmentLoader_.playlist(media, this.requestOptions_);
154 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_); 169 this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_);
155 this.mainSegmentLoader_.load(); 170 this.mainSegmentLoader_.load();
156 171
...@@ -340,7 +355,7 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -340,7 +355,7 @@ export default class MasterPlaylistController extends videojs.EventTarget {
340 let media = this.audioPlaylistLoader_.media(); 355 let media = this.audioPlaylistLoader_.media();
341 /* eslint-enable no-shadow */ 356 /* eslint-enable no-shadow */
342 357
343 this.audioSegmentLoader_.playlist(media); 358 this.audioSegmentLoader_.playlist(media, this.requestOptions_);
344 this.addMimeType_(this.audioSegmentLoader_, 'mp4a.40.2', media); 359 this.addMimeType_(this.audioSegmentLoader_, 'mp4a.40.2', media);
345 360
346 // if the video is already playing, or if this isn't a live video and preload 361 // if the video is already playing, or if this isn't a live video and preload
...@@ -370,7 +385,7 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -370,7 +385,7 @@ export default class MasterPlaylistController extends videojs.EventTarget {
370 return; 385 return;
371 } 386 }
372 387
373 this.audioSegmentLoader_.playlist(updatedPlaylist); 388 this.audioSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
374 }); 389 });
375 390
376 this.audioPlaylistLoader_.on('error', () => { 391 this.audioPlaylistLoader_.on('error', () => {
......
...@@ -177,6 +177,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) { ...@@ -177,6 +177,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
177 // merge this playlist into the master 177 // merge this playlist into the master
178 update = updateMaster(loader.master, parser.manifest); 178 update = updateMaster(loader.master, parser.manifest);
179 refreshDelay = (parser.manifest.targetDuration || 10) * 1000; 179 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
180 loader.targetDuration = parser.manifest.targetDuration;
180 if (update) { 181 if (update) {
181 loader.master = update; 182 loader.master = update;
182 loader.updateMediaPlaylist_(parser.manifest); 183 loader.updateMediaPlaylist_(parser.manifest);
...@@ -227,6 +228,44 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) { ...@@ -227,6 +228,44 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
227 } 228 }
228 }; 229 };
229 230
231 /**
232 * Returns the number of enabled playlists on the master playlist object
233 *
234 * @return {Number} number of eneabled playlists
235 */
236 loader.enabledPlaylists_ = function() {
237 return loader.master.playlists.filter((element, index, array) => {
238 return !element.excludeUntil || element.excludeUntil <= Date.now();
239 }).length;
240 };
241
242 /**
243 * Returns whether the current playlist is the lowest rendition
244 *
245 * @return {Boolean} true if on lowest rendition
246 */
247 loader.isLowestEnabledRendition_ = function() {
248 if (!loader.media()) {
249 return false;
250 }
251
252 let currentPlaylist = loader.media().attributes.BANDWIDTH;
253
254 return !(loader.master.playlists.filter((element, index, array) => {
255 let enabled = typeof element.excludeUntil === 'undefined' ||
256 element.excludeUntil <= Date.now();
257
258 if (!enabled) {
259 return false;
260 }
261
262 let item = element.attributes.BANDWIDTH;
263
264 return item <= currentPlaylist;
265
266 }).length > 1);
267 };
268
230 /** 269 /**
231 * When called without any arguments, returns the currently 270 * When called without any arguments, returns the currently
232 * active media playlist. When called with a single argument, 271 * active media playlist. When called with a single argument,
......
...@@ -140,7 +140,6 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -140,7 +140,6 @@ export default class SegmentLoader extends videojs.EventTarget {
140 this.seeking_ = settings.seeking; 140 this.seeking_ = settings.seeking;
141 this.setCurrentTime_ = settings.setCurrentTime; 141 this.setCurrentTime_ = settings.setCurrentTime;
142 this.mediaSource_ = settings.mediaSource; 142 this.mediaSource_ = settings.mediaSource;
143 this.withCredentials_ = settings.withCredentials;
144 this.checkBufferTimeout_ = null; 143 this.checkBufferTimeout_ = null;
145 this.error_ = void 0; 144 this.error_ = void 0;
146 this.expired_ = 0; 145 this.expired_ = 0;
...@@ -150,6 +149,7 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -150,6 +149,7 @@ export default class SegmentLoader extends videojs.EventTarget {
150 this.pendingSegment_ = null; 149 this.pendingSegment_ = null;
151 this.sourceUpdater_ = null; 150 this.sourceUpdater_ = null;
152 this.hls_ = settings.hls; 151 this.hls_ = settings.hls;
152 this.xhrOptions_ = null;
153 } 153 }
154 154
155 /** 155 /**
...@@ -238,8 +238,10 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -238,8 +238,10 @@ export default class SegmentLoader extends videojs.EventTarget {
238 * 238 *
239 * @param {PlaylistLoader} media the playlist to set on the segment loader 239 * @param {PlaylistLoader} media the playlist to set on the segment loader
240 */ 240 */
241 playlist(media) { 241 playlist(media, options = {}) {
242 this.playlist_ = media; 242 this.playlist_ = media;
243 this.xhrOptions_ = options;
244
243 // if we were unpaused but waiting for a playlist, start 245 // if we were unpaused but waiting for a playlist, start
244 // buffering now 246 // buffering now
245 if (this.sourceUpdater_ && 247 if (this.sourceUpdater_ &&
...@@ -506,7 +508,6 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -506,7 +508,6 @@ export default class SegmentLoader extends videojs.EventTarget {
506 */ 508 */
507 loadSegment_(segmentInfo) { 509 loadSegment_(segmentInfo) {
508 let segment; 510 let segment;
509 let requestTimeout;
510 let keyXhr; 511 let keyXhr;
511 let segmentXhr; 512 let segmentXhr;
512 let seekable = this.seekable_(); 513 let seekable = this.seekable_();
...@@ -534,27 +535,25 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -534,27 +535,25 @@ export default class SegmentLoader extends videojs.EventTarget {
534 } 535 }
535 536
536 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; 537 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
537 // Set xhr timeout to 150% of the segment duration to allow us
538 // some time to switch renditions in the event of a catastrophic
539 // decrease in network performance or a server issue.
540 requestTimeout = (segment.duration * 1.5) * 1000;
541 538
542 if (segment.key) { 539 if (segment.key) {
543 keyXhr = this.hls_.xhr({ 540 let keyRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
544 uri: segment.key.resolvedUri, 541 uri: segment.key.resolvedUri,
545 responseType: 'arraybuffer', 542 responseType: 'arraybuffer'
546 withCredentials: this.withCredentials_, 543 });
547 timeout: requestTimeout 544
548 }, this.handleResponse_.bind(this)); 545 keyXhr = this.hls_.xhr(keyRequestOptions, this.handleResponse_.bind(this));
549 } 546 }
547
550 this.pendingSegment_ = segmentInfo; 548 this.pendingSegment_ = segmentInfo;
551 segmentXhr = this.hls_.xhr({ 549
550 let segmentRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
552 uri: segmentInfo.uri, 551 uri: segmentInfo.uri,
553 responseType: 'arraybuffer', 552 responseType: 'arraybuffer',
554 withCredentials: this.withCredentials_,
555 timeout: requestTimeout,
556 headers: segmentXhrHeaders(segment) 553 headers: segmentXhrHeaders(segment)
557 }, this.handleResponse_.bind(this)); 554 });
555
556 segmentXhr = this.hls_.xhr(segmentRequestOptions, this.handleResponse_.bind(this));
558 557
559 this.xhr_ = { 558 this.xhr_ = {
560 keyXhr, 559 keyXhr,
......
...@@ -484,6 +484,40 @@ QUnit.test('updates the duration after switching playlists', function() { ...@@ -484,6 +484,40 @@ QUnit.test('updates the duration after switching playlists', function() {
484 '16 bytes downloaded'); 484 '16 bytes downloaded');
485 }); 485 });
486 486
487 QUnit.test('removes request timeout when segment timesout on lowest rendition',
488 function() {
489 this.masterPlaylistController.mediaSource.trigger('sourceopen');
490
491 // master
492 standardXHRResponse(this.requests[0]);
493 // media
494 standardXHRResponse(this.requests[1]);
495
496 QUnit.equal(this.masterPlaylistController.requestOptions_.timeout,
497 this.masterPlaylistController.masterPlaylistLoader_.targetDuration * 1.5 *
498 1000,
499 'default request timeout');
500
501 QUnit.ok(!this.masterPlaylistController
502 .masterPlaylistLoader_
503 .isLowestEnabledRendition_(), 'Not lowest rendition');
504
505 // Cause segment to timeout to force player into lowest rendition
506 this.requests[2].timedout = true;
507
508 // Downloading segment should cause media change and timeout removal
509 // segment 0
510 standardXHRResponse(this.requests[2]);
511 // Download new segment after media change
512 standardXHRResponse(this.requests[3]);
513
514 QUnit.ok(this.masterPlaylistController
515 .masterPlaylistLoader_.isLowestEnabledRendition_(), 'On lowest rendition');
516
517 QUnit.equal(this.masterPlaylistController.requestOptions_.timeout, 0,
518 'request timeout 0');
519 });
520
487 QUnit.test('seekable uses the intersection of alternate audio and combined tracks', 521 QUnit.test('seekable uses the intersection of alternate audio and combined tracks',
488 function() { 522 function() {
489 let origSeekable = Playlist.seekable; 523 let origSeekable = Playlist.seekable;
......
...@@ -121,6 +121,48 @@ QUnit.test('resolves relative media playlist URIs', function() { ...@@ -121,6 +121,48 @@ QUnit.test('resolves relative media playlist URIs', function() {
121 'resolved media URI'); 121 'resolved media URI');
122 }); 122 });
123 123
124 QUnit.test('playlist loader returns the correct amount of enabled playlists', function() {
125 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
126
127 loader.load();
128
129 this.requests.shift().respond(200, null,
130 '#EXTM3U\n' +
131 '#EXT-X-STREAM-INF:\n' +
132 'video1/media.m3u8\n' +
133 '#EXT-X-STREAM-INF:\n' +
134 'video2/media.m3u8\n');
135 QUnit.equal(loader.enabledPlaylists_(), 2, 'Returned initial amount of playlists');
136 loader.master.playlists[0].excludeUntil = Date.now() + 100000;
137 this.clock.tick(1000);
138 QUnit.equal(loader.enabledPlaylists_(), 1, 'Returned one less playlist');
139 });
140
141 QUnit.test('playlist loader detects if we are on lowest rendition', function() {
142 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
143
144 loader.load();
145 this.requests.shift().respond(200, null,
146 '#EXTM3U\n' +
147 '#EXT-X-STREAM-INF:\n' +
148 'video1/media.m3u8\n' +
149 '#EXT-X-STREAM-INF:\n' +
150 'video2/media.m3u8\n');
151 loader.media = function() {
152 return {attributes: {BANDWIDTH: 10}};
153 };
154
155 loader.master.playlists = [{attributes: {BANDWIDTH: 10}},
156 {attributes: {BANDWIDTH: 20}}];
157 QUnit.ok(loader.isLowestEnabledRendition_(), 'Detected on lowest rendition');
158
159 loader.media = function() {
160 return {attributes: {BANDWIDTH: 20}};
161 };
162
163 QUnit.ok(!loader.isLowestEnabledRendition_(), 'Detected not on lowest rendition');
164 });
165
124 QUnit.test('recognizes absolute URIs and requests them unmodified', function() { 166 QUnit.test('recognizes absolute URIs and requests them unmodified', function() {
125 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls); 167 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
126 168
......
...@@ -647,77 +647,6 @@ QUnit.test('live playlists do not trigger ended', function() { ...@@ -647,77 +647,6 @@ QUnit.test('live playlists do not trigger ended', function() {
647 QUnit.equal(loader.mediaRequests, 1, '1 request'); 647 QUnit.equal(loader.mediaRequests, 1, '1 request');
648 }); 648 });
649 649
650 QUnit.test('respects the global withCredentials option', function() {
651 let hlsOptions = videojs.options.hls;
652
653 videojs.options.hls = {
654 withCredentials: true
655 };
656 loader = new SegmentLoader({
657 hls: this.fakeHls,
658 currentTime() {
659 return currentTime;
660 },
661 seekable: () => this.seekable,
662 mediaSource
663 });
664 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
665 loader.mimeType(this.mimeType);
666 loader.load();
667
668 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
669 QUnit.ok(this.requests[0].withCredentials, 'key request used withCredentials');
670 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
671 QUnit.ok(this.requests[1].withCredentials, 'segment request used withCredentials');
672 videojs.options.hls = hlsOptions;
673 });
674
675 QUnit.test('respects the withCredentials option', function() {
676 loader = new SegmentLoader({
677 hls: this.fakeHls,
678 currentTime() {
679 return currentTime;
680 },
681 seekable: () => this.seekable,
682 mediaSource,
683 withCredentials: true
684 });
685 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
686 loader.mimeType(this.mimeType);
687 loader.load();
688
689 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
690 QUnit.ok(this.requests[0].withCredentials, 'key request used withCredentials');
691 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
692 QUnit.ok(this.requests[1].withCredentials, 'segment request used withCredentials');
693 });
694
695 QUnit.test('the withCredentials option overrides the global', function() {
696 let hlsOptions = videojs.options.hls;
697
698 videojs.options.hls = {
699 withCredentials: true
700 };
701 loader = new SegmentLoader({
702 hls: this.fakeHls,
703 currentTime() {
704 return currentTime;
705 },
706 mediaSource,
707 seekable: () => this.seekable,
708 withCredentials: false
709 });
710 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
711 loader.mimeType(this.mimeType);
712 loader.load();
713
714 QUnit.equal(this.requests[0].url, '0-key.php', 'requested the first segment\'s key');
715 QUnit.ok(!this.requests[0].withCredentials, 'overrode key request withCredentials');
716 QUnit.equal(this.requests[1].url, '0.ts', 'requested the first segment');
717 QUnit.ok(!this.requests[1].withCredentials, 'overrode segment request withCredentials');
718 videojs.options.hls = hlsOptions;
719 });
720
721 QUnit.test('remains ready if there are no segments', function() { 650 QUnit.test('remains ready if there are no segments', function() {
722 loader.playlist(playlistWithDuration(0)); 651 loader.playlist(playlistWithDuration(0));
723 loader.mimeType(this.mimeType); 652 loader.mimeType(this.mimeType);
......
...@@ -1219,6 +1219,25 @@ QUnit.test('if withCredentials global option is used, withCredentials is set on ...@@ -1219,6 +1219,25 @@ QUnit.test('if withCredentials global option is used, withCredentials is set on
1219 videojs.options.hls = hlsOptions; 1219 videojs.options.hls = hlsOptions;
1220 }); 1220 });
1221 1221
1222 QUnit.test('the withCredentials option overrides the global default', function() {
1223 let hlsOptions = videojs.options.hls;
1224
1225 this.player.dispose();
1226 videojs.options.hls = {
1227 withCredentials: true
1228 };
1229 this.player = createPlayer();
1230 this.player.src({
1231 src: 'http://example.com/media.m3u8',
1232 type: 'application/vnd.apple.mpegurl',
1233 withCredentials: false
1234 });
1235 openMediaSource(this.player, this.clock);
1236 QUnit.ok(!this.requests[0].withCredentials,
1237 'with credentials should be set to false if if overrode global option');
1238 videojs.options.hls = hlsOptions;
1239 });
1240
1222 QUnit.test('if mode global option is used, mode is set to global option', function() { 1241 QUnit.test('if mode global option is used, mode is set to global option', function() {
1223 let hlsOptions = videojs.options.hls; 1242 let hlsOptions = videojs.options.hls;
1224 1243
......