Skip to content
This repository was archived by the owner on Jan 12, 2019. It is now read-only.

Commit 7186be0

Browse files
SleepWalkergesinger
authored andcommitted
Add redirect support for manifest and media requests (#1213)
* Add redirect support for manifest and media requests (#912) * Add an option to enable manifest redirects support (#912)
1 parent a3ecfd0 commit 7186be0

9 files changed

+221
-51
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,16 @@ is set to `true`.
243243
See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/)
244244
for more info.
245245

246+
##### handleManifestRedirects
247+
* Type: `boolean`
248+
* Default: `false`
249+
* can be used as a source option
250+
* can be used as an initialization option
251+
252+
When the `handleManifestRedirects` property is set to `true`, manifest requests
253+
which are redirected will have their URL updated to the new URL for future
254+
requests.
255+
246256
##### useCueTags
247257
* Type: `boolean`
248258
* can be used as an initialization option

src/master-playlist-controller.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export const mimeTypesForPlaylist_ = function(master, media) {
223223
};
224224

225225
/**
226-
* the master playlist controller controller all interactons
226+
* the master playlist controller controls all interactons
227227
* between playlists and segmentloaders. At this time this mainly
228228
* involves a master playlist and a series of audio playlists
229229
* if they are available
@@ -237,6 +237,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
237237

238238
let {
239239
url,
240+
handleManifestRedirects,
240241
withCredentials,
241242
mode,
242243
tech,
@@ -253,21 +254,22 @@ export class MasterPlaylistController extends videojs.EventTarget {
253254

254255
Hls = externHls;
255256

256-
this.withCredentials = withCredentials;
257257
this.tech_ = tech;
258258
this.hls_ = tech.hls;
259259
this.mode_ = mode;
260260
this.useCueTags_ = useCueTags;
261261
this.blacklistDuration = blacklistDuration;
262262
this.enableLowInitialPlaylist = enableLowInitialPlaylist;
263+
263264
if (this.useCueTags_) {
264265
this.cueTagsTrack_ = this.tech_.addTextTrack('metadata',
265266
'ad-cues');
266267
this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
267268
}
268269

269270
this.requestOptions_ = {
270-
withCredentials: this.withCredentials,
271+
withCredentials,
272+
handleManifestRedirects,
271273
timeout: null
272274
};
273275

@@ -304,7 +306,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
304306
};
305307

306308
// setup playlist loaders
307-
this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials);
309+
this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.requestOptions_);
308310
this.setupMasterPlaylistLoaderListeners_();
309311

310312
// setup segment loaders

src/media-groups.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ export const initialize = {
352352
mode,
353353
hls,
354354
segmentLoaders: { [type]: segmentLoader },
355-
requestOptions: { withCredentials },
355+
requestOptions,
356356
master: { mediaGroups },
357357
mediaTypes: {
358358
[type]: {
@@ -383,7 +383,7 @@ export const initialize = {
383383
if (properties.resolvedUri) {
384384
playlistLoader = new PlaylistLoader(properties.resolvedUri,
385385
hls,
386-
withCredentials);
386+
requestOptions);
387387
} else {
388388
// no resolvedUri means the audio is muxed with the video when using this
389389
// audio track
@@ -429,7 +429,7 @@ export const initialize = {
429429
tech,
430430
hls,
431431
segmentLoaders: { [type]: segmentLoader },
432-
requestOptions: { withCredentials },
432+
requestOptions,
433433
master: { mediaGroups },
434434
mediaTypes: {
435435
[type]: {
@@ -463,7 +463,7 @@ export const initialize = {
463463
id: variantLabel,
464464
playlistLoader: new PlaylistLoader(properties.resolvedUri,
465465
hls,
466-
withCredentials)
466+
requestOptions)
467467
}, properties);
468468

469469
setupListeners[type](type, properties.playlistLoader, settings);

src/playlist-loader.js

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
11
/**
2-
* @file playlist-loader.js
2+
* @module playlist-loader
33
*
4-
* A state machine that manages the loading, caching, and updating of
4+
* @file A state machine that manages the loading, caching, and updating of
55
* M3U8 playlists.
6-
*
76
*/
87
import resolveUrl from './resolve-url';
98
import { mergeOptions, EventTarget, log } from 'video.js';
109
import m3u8 from 'm3u8-parser';
1110
import window from 'global/window';
1211

1312
/**
14-
* Returns a new array of segments that is the result of merging
15-
* properties from an older list of segments onto an updated
16-
* list. No properties on the updated playlist will be overridden.
17-
*
18-
* @param {Array} original the outdated list of segments
19-
* @param {Array} update the updated list of segments
20-
* @param {Number=} offset the index of the first update
21-
* segment in the original segment list. For non-live playlists,
22-
* this should always be zero and does not need to be
23-
* specified. For live playlists, it should be the difference
24-
* between the media sequence numbers in the original and updated
25-
* playlists.
26-
* @return a list of merged segment objects
27-
*/
13+
* Returns a new array of segments that is the result of merging
14+
* properties from an older list of segments onto an updated
15+
* list. No properties on the updated playlist will be overridden.
16+
*
17+
* @param {Array} original the outdated list of segments
18+
* @param {Array} update the updated list of segments
19+
* @param {Number=} offset the index of the first update
20+
* segment in the original segment list. For non-live playlists,
21+
* this should always be zero and does not need to be
22+
* specified. For live playlists, it should be the difference
23+
* between the media sequence numbers in the original and updated
24+
* playlists.
25+
* @return a list of merged segment objects
26+
*/
2827
export const updateSegments = (original, update, offset) => {
2928
const result = update.slice();
3029

@@ -172,18 +171,24 @@ export const refreshDelay = (media, update) => {
172171
* Load a playlist from a remote location
173172
*
174173
* @class PlaylistLoader
175-
* @extends Stream
174+
* @extends videojs.EventTarget
176175
* @param {String} srcUrl the url to start with
177-
* @param {Boolean} withCredentials the withCredentials xhr option
178-
* @constructor
176+
* @param {Object} hls
177+
* @param {Object} [options]
178+
* @param {Boolean} [options.withCredentials=false] the withCredentials xhr option
179+
* @param {Boolean} [options.handleManifestRedirects=false] whether to follow redirects, when any
180+
* playlist request was redirected
179181
*/
180182
export default class PlaylistLoader extends EventTarget {
181-
constructor(srcUrl, hls, withCredentials) {
183+
constructor(srcUrl, hls, options) {
182184
super();
183185

186+
options = options || {};
187+
184188
this.srcUrl = srcUrl;
185189
this.hls_ = hls;
186-
this.withCredentials = withCredentials;
190+
this.withCredentials = !!options.withCredentials;
191+
this.handleManifestRedirects = !!options.handleManifestRedirects;
187192

188193
if (!this.srcUrl) {
189194
throw new Error('A non-empty playlist URL is required');
@@ -360,7 +365,7 @@ export default class PlaylistLoader extends EventTarget {
360365

361366
// there is already an outstanding playlist request
362367
if (this.request) {
363-
if (resolveUrl(this.master.uri, playlist.uri) === this.request.url) {
368+
if (playlist.resolvedUri === this.request.url) {
364369
// requesting to switch to the same playlist multiple times
365370
// has no effect after the first
366371
return;
@@ -376,14 +381,16 @@ export default class PlaylistLoader extends EventTarget {
376381
}
377382

378383
this.request = this.hls_.xhr({
379-
uri: resolveUrl(this.master.uri, playlist.uri),
384+
uri: playlist.resolvedUri,
380385
withCredentials: this.withCredentials
381386
}, (error, req) => {
382387
// disposed
383388
if (!this.request) {
384389
return;
385390
}
386391

392+
playlist.resolvedUri = this.resolveManifestRedirect(playlist.resolvedUri, req);
393+
387394
if (error) {
388395
return this.playlistRequestError(this.request, playlist.uri, startingState);
389396
}
@@ -399,6 +406,28 @@ export default class PlaylistLoader extends EventTarget {
399406
});
400407
}
401408

409+
/**
410+
* Checks whether xhr request was redirected and returns correct url depending
411+
* on `handleManifestRedirects` option
412+
*
413+
* @api private
414+
*
415+
* @param {String} url - an url being requested
416+
* @param {XMLHttpRequest} req - xhr request result
417+
*
418+
* @return {String}
419+
*/
420+
resolveManifestRedirect(url, req) {
421+
if (this.handleManifestRedirects &&
422+
req.responseURL &&
423+
url !== req.responseURL
424+
) {
425+
return req.responseURL;
426+
}
427+
428+
return url;
429+
}
430+
402431
/**
403432
* pause loading of the playlist
404433
*/
@@ -492,6 +521,8 @@ export default class PlaylistLoader extends EventTarget {
492521

493522
this.state = 'HAVE_MASTER';
494523

524+
this.srcUrl = this.resolveManifestRedirect(this.srcUrl, req);
525+
495526
parser.manifest.uri = this.srcUrl;
496527

497528
// loaded a master playlist
@@ -521,14 +552,14 @@ export default class PlaylistLoader extends EventTarget {
521552
},
522553
uri: window.location.href,
523554
playlists: [{
524-
uri: this.srcUrl
555+
uri: this.srcUrl,
556+
resolvedUri: this.srcUrl,
557+
// m3u8-parser does not attach an attributes property to media playlists so make
558+
// sure that the property is attached to avoid undefined reference errors
559+
attributes: {}
525560
}]
526561
};
527562
this.master.playlists[this.srcUrl] = this.master.playlists[0];
528-
this.master.playlists[0].resolvedUri = this.srcUrl;
529-
// m3u8-parser does not attach an attributes property to media playlists so make
530-
// sure that the property is attached to avoid undefined reference errors
531-
this.master.playlists[0].attributes = this.master.playlists[0].attributes || {};
532563
this.haveMetadata(req, this.srcUrl);
533564
return this.trigger('loadedmetadata');
534565
});

src/videojs-contrib-hls.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ class HlsHandler extends Component {
260260
this.options_.bandwidth === INITIAL_BANDWIDTH;
261261

262262
// grab options passed to player.src
263-
['withCredentials', 'bandwidth'].forEach((option) => {
263+
['withCredentials', 'bandwidth', 'handleManifestRedirects'].forEach((option) => {
264264
if (typeof this.source_[option] !== 'undefined') {
265265
this.options_[option] = this.source_[option];
266266
}

test/master-playlist-controller.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,26 @@ QUnit.test('obeys none preload option', function(assert) {
136136
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
137137
});
138138

139+
QUnit.test('passes options to PlaylistLoader', function(assert) {
140+
const options = {
141+
url: 'test',
142+
tech: this.player.tech_
143+
};
144+
145+
let controller = new MasterPlaylistController(options);
146+
147+
assert.notOk(controller.masterPlaylistLoader_.withCredentials, 'credentials wont be sent by default');
148+
assert.notOk(controller.masterPlaylistLoader_.handleManifestRedirects, 'redirects are ignored by default');
149+
150+
controller = new MasterPlaylistController(Object.assign({
151+
withCredentials: true,
152+
handleManifestRedirects: true
153+
}, options));
154+
155+
assert.ok(controller.masterPlaylistLoader_.withCredentials, 'withCredentials enabled');
156+
assert.ok(controller.masterPlaylistLoader_.handleManifestRedirects, 'handleManifestRedirects enabled');
157+
});
158+
139159
QUnit.test('obeys auto preload option', function(assert) {
140160
this.player.preload('auto');
141161
// master

test/playlist-loader.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,68 @@ QUnit.test('recognizes domain-relative URLs', function(assert) {
956956
'resolved segment URI');
957957
});
958958

959+
QUnit.test('recognizes redirect, when manifest requested', function(assert) {
960+
let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls, {
961+
handleManifestRedirects: true
962+
});
963+
964+
loader.load();
965+
966+
const manifestRequest = this.requests.shift();
967+
968+
manifestRequest.responseURL = window.location.protocol + '//' +
969+
'foo-bar.com/manifest/media.m3u8';
970+
manifestRequest.respond(200, null,
971+
'#EXTM3U\n' +
972+
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
973+
'/media.m3u8\n');
974+
assert.equal(loader.master.playlists[0].resolvedUri,
975+
window.location.protocol + '//' +
976+
'foo-bar.com/media.m3u8',
977+
'resolved media URI');
978+
979+
this.requests.shift().respond(200, null,
980+
'#EXTM3U\n' +
981+
'#EXTINF:10,\n' +
982+
'/00001.ts\n' +
983+
'#EXT-X-ENDLIST\n');
984+
assert.equal(loader.media().segments[0].resolvedUri,
985+
window.location.protocol + '//' +
986+
'foo-bar.com/00001.ts',
987+
'resolved segment URI');
988+
});
989+
990+
QUnit.test('recognizes redirect, when media requested', function(assert) {
991+
let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls, {
992+
handleManifestRedirects: true
993+
});
994+
995+
loader.load();
996+
997+
this.requests.shift().respond(200, null,
998+
'#EXTM3U\n' +
999+
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1000+
'/media.m3u8\n');
1001+
assert.equal(loader.master.playlists[0].resolvedUri,
1002+
window.location.protocol + '//' +
1003+
window.location.host + '/media.m3u8',
1004+
'resolved media URI');
1005+
1006+
const mediaRequest = this.requests.shift();
1007+
1008+
mediaRequest.responseURL = window.location.protocol + '//' +
1009+
'foo-bar.com/media.m3u8';
1010+
mediaRequest.respond(200, null,
1011+
'#EXTM3U\n' +
1012+
'#EXTINF:10,\n' +
1013+
'/00001.ts\n' +
1014+
'#EXT-X-ENDLIST\n');
1015+
assert.equal(loader.media().segments[0].resolvedUri,
1016+
window.location.protocol + '//' +
1017+
'foo-bar.com/00001.ts',
1018+
'resolved segment URI');
1019+
});
1020+
9591021
QUnit.test('recognizes key URLs relative to master and playlist', function(assert) {
9601022
let loader = new PlaylistLoader('/video/media-encrypted.m3u8', this.fakeHls);
9611023

0 commit comments

Comments
 (0)