Skip to content

Commit

Permalink
Migrate HLS redirect logic from contrib-hls PR; Add logic for DASH (#412
Browse files Browse the repository at this point in the history
)
  • Loading branch information
OshinKaramian authored Mar 6, 2019
1 parent 90093bc commit b820f24
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 39 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ is set to `true`.
See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/)
for more info.

##### handleManifestRedirects
* Type: `boolean`
* Default: `false`
* can be used as a source option
* can be used as an initialization option

When the `handleManifestRedirects` property is set to `true`, manifest requests
which are redirected will have their URL updated to the new URL for future
requests.

##### useCueTags
* Type: `boolean`
* can be used as an initialization option
Expand Down
11 changes: 9 additions & 2 deletions src/dash-playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
updateMaster as updatePlaylist,
forEachMediaGroup
} from './playlist-loader';
import resolveUrl from './resolve-url';
import { resolveUrl, resolveManifestRedirect } from './resolve-url';
import window from 'global/window';

const { EventTarget, mergeOptions } = videojs;
Expand Down Expand Up @@ -69,11 +69,14 @@ export default class DashPlaylistLoader extends EventTarget {
// DashPlaylistLoader must accept either a src url or a playlist because subsequent
// playlist loader setups from media groups will expect to be able to pass a playlist
// (since there aren't external URLs to media playlists with DASH)
constructor(srcUrlOrPlaylist, hls, withCredentials, masterPlaylistLoader) {
constructor(srcUrlOrPlaylist, hls, options = { }, masterPlaylistLoader) {
super();

const { withCredentials = false, handleManifestRedirects = false } = options;

this.hls_ = hls;
this.withCredentials = withCredentials;
this.handleManifestRedirects = handleManifestRedirects;

if (!srcUrlOrPlaylist) {
throw new Error('A non-empty playlist URL or playlist is required');
Expand Down Expand Up @@ -327,6 +330,8 @@ export default class DashPlaylistLoader extends EventTarget {
this.masterLoaded_ = Date.now();
}

this.srcUrl = resolveManifestRedirect(this.handleManifestRedirects, this.srcUrl, req);

this.syncClientServerClock_(this.onClientServerClockSync_.bind(this));
});
}
Expand Down Expand Up @@ -437,6 +442,8 @@ export default class DashPlaylistLoader extends EventTarget {
* TODO: Does the client offset need to be recalculated when the xml is refreshed?
*/
refreshXml_() {
// The srcUrl here *may* need to pass through handleManifestsRedirects when
// sidx is implemented
this.request = this.hls_.xhr({
uri: this.srcUrl,
withCredentials: this.withCredentials
Expand Down
8 changes: 5 additions & 3 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class MasterPlaylistController extends videojs.EventTarget {

let {
url,
handleManifestRedirects,
withCredentials,
tech,
bandwidth,
Expand Down Expand Up @@ -87,7 +88,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
}

this.requestOptions_ = {
withCredentials: this.withCredentials,
withCredentials,
handleManifestRedirects,
timeout: null
};

Expand Down Expand Up @@ -127,8 +129,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
};

this.masterPlaylistLoader_ = this.sourceType_ === 'dash' ?
new DashPlaylistLoader(url, this.hls_, this.withCredentials) :
new PlaylistLoader(url, this.hls_, this.withCredentials);
new DashPlaylistLoader(url, this.hls_, this.requestOptions_) :
new PlaylistLoader(url, this.hls_, this.requestOptions_);
this.setupMasterPlaylistLoaderListeners_();

// setup segment loaders
Expand Down
12 changes: 6 additions & 6 deletions src/media-groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ export const initialize = {
hls,
sourceType,
segmentLoaders: { [type]: segmentLoader },
requestOptions: { withCredentials },
requestOptions,
master: { mediaGroups, playlists },
mediaTypes: {
[type]: {
Expand Down Expand Up @@ -404,11 +404,11 @@ export const initialize = {
if (properties.resolvedUri) {
playlistLoader = new PlaylistLoader(properties.resolvedUri,
hls,
withCredentials);
requestOptions);
} else if (properties.playlists && sourceType === 'dash') {
playlistLoader = new DashPlaylistLoader(properties.playlists[0],
hls,
withCredentials,
requestOptions,
masterPlaylistLoader);
} else {
// no resolvedUri means the audio is muxed with the video when using this
Expand Down Expand Up @@ -456,7 +456,7 @@ export const initialize = {
hls,
sourceType,
segmentLoaders: { [type]: segmentLoader },
requestOptions: { withCredentials },
requestOptions,
master: { mediaGroups },
mediaTypes: {
[type]: {
Expand Down Expand Up @@ -491,11 +491,11 @@ export const initialize = {

if (sourceType === 'hls') {
playlistLoader =
new PlaylistLoader(properties.resolvedUri, hls, withCredentials);
new PlaylistLoader(properties.resolvedUri, hls, requestOptions);
} else if (sourceType === 'dash') {
playlistLoader = new DashPlaylistLoader(properties.playlists[0],
hls,
withCredentials,
requestOptions,
masterPlaylistLoader);
}

Expand Down
31 changes: 19 additions & 12 deletions src/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* M3U8 playlists.
*
*/
import resolveUrl from './resolve-url';
import { resolveUrl, resolveManifestRedirect } from './resolve-url';
import videojs from 'video.js';
import { Parser as M3u8Parser } from 'm3u8-parser';
import window from 'global/window';
Expand Down Expand Up @@ -197,17 +197,20 @@ export const refreshDelay = (media, update) => {
* @constructor
*/
export default class PlaylistLoader extends EventTarget {
constructor(srcUrl, hls, withCredentials) {
constructor(srcUrl, hls, options = { }) {
super();

const { withCredentials = false, handleManifestRedirects = false } = options;

this.srcUrl = srcUrl;
this.hls_ = hls;
this.withCredentials = withCredentials;
this.handleManifestRedirects = handleManifestRedirects;

const options = hls.options_;
const hlsOptions = hls.options_;

this.customTagParsers = (options && options.customTagParsers) || [];
this.customTagMappers = (options && options.customTagMappers) || [];
this.customTagParsers = (hlsOptions && hlsOptions.customTagParsers) || [];
this.customTagMappers = (hlsOptions && hlsOptions.customTagMappers) || [];

if (!this.srcUrl) {
throw new Error('A non-empty playlist URL is required');
Expand Down Expand Up @@ -390,7 +393,7 @@ export default class PlaylistLoader extends EventTarget {

// there is already an outstanding playlist request
if (this.request) {
if (resolveUrl(this.master.uri, playlist.uri) === this.request.url) {
if (playlist.resolvedUri === this.request.url) {
// requesting to switch to the same playlist multiple times
// has no effect after the first
return;
Expand All @@ -406,14 +409,16 @@ export default class PlaylistLoader extends EventTarget {
}

this.request = this.hls_.xhr({
uri: resolveUrl(this.master.uri, playlist.uri),
uri: playlist.resolvedUri,
withCredentials: this.withCredentials
}, (error, req) => {
// disposed
if (!this.request) {
return;
}

playlist.resolvedUri = resolveManifestRedirect(this.handleManifestRedirects, playlist.resolvedUri, req);

if (error) {
return this.playlistRequestError(this.request, playlist.uri, startingState);
}
Expand Down Expand Up @@ -528,6 +533,8 @@ export default class PlaylistLoader extends EventTarget {

this.state = 'HAVE_MASTER';

this.srcUrl = resolveManifestRedirect(this.handleManifestRedirects, this.srcUrl, req);

parser.manifest.uri = this.srcUrl;

// loaded a master playlist
Expand Down Expand Up @@ -558,14 +565,14 @@ export default class PlaylistLoader extends EventTarget {
uri: window.location.href,
playlists: [{
uri: this.srcUrl,
id: 0
id: 0,
resolvedUri: this.srcUrl,
// m3u8-parser does not attach an attributes property to media playlists so make
// sure that the property is attached to avoid undefined reference errors
attributes: {}
}]
};
this.master.playlists[this.srcUrl] = this.master.playlists[0];
this.master.playlists[0].resolvedUri = this.srcUrl;
// m3u8-parser does not attach an attributes property to media playlists so make
// sure that the property is attached to avoid undefined reference errors
this.master.playlists[0].attributes = this.master.playlists[0].attributes || {};
this.haveMetadata(req, this.srcUrl);
return this.trigger('loadedmetadata');
});
Expand Down
28 changes: 26 additions & 2 deletions src/resolve-url.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* @file resolve-url.js
* @file resolve-url.js - Handling how URLs are resolved and manipulated
*/

import URLToolkit from 'url-toolkit';
import window from 'global/window';

const resolveUrl = function(baseURL, relativeURL) {
export const resolveUrl = function(baseURL, relativeURL) {
// return early if we don't need to resolve
if ((/^[a-z]+:/i).test(relativeURL)) {
return relativeURL;
Expand All @@ -19,4 +19,28 @@ const resolveUrl = function(baseURL, relativeURL) {
return URLToolkit.buildAbsoluteURL(baseURL, relativeURL);
};

/**
* Checks whether xhr request was redirected and returns correct url depending
* on `handleManifestRedirects` option
*
* @api private
*
* @param {String} url - an url being requested
* @param {XMLHttpRequest} req - xhr request result
*
* @return {String}
*/
export const resolveManifestRedirect = (handleManifestRedirect, url, req) => {
// To understand how the responseURL below is set and generated:
// - https://fetch.spec.whatwg.org/#concept-response-url
// - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
if (handleManifestRedirect && req.responseURL &&
url !== req.responseURL
) {
return req.responseURL;
}

return url;
};

export default resolveUrl;
4 changes: 3 additions & 1 deletion src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ class HlsHandler extends Component {
setOptions_() {
// defaults
this.options_.withCredentials = this.options_.withCredentials || false;
this.options_.handleManifestRedirects = this.options_.handleManifestRedirects || false;
this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true;
this.options_.smoothQualityChange = this.options_.smoothQualityChange || false;
this.options_.useBandwidthFromLocalStorage =
Expand Down Expand Up @@ -441,7 +442,8 @@ class HlsHandler extends Component {
'bandwidth',
'smoothQualityChange',
'customTagParsers',
'customTagMappers'
'customTagMappers',
'handleManifestRedirects'
].forEach((option) => {
if (typeof this.source_[option] !== 'undefined') {
this.options_[option] = this.source_[option];
Expand Down
46 changes: 46 additions & 0 deletions test/dash-playlist-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,52 @@ QUnit.test('requests the manifest immediately when given a URL', function(assert
assert.equal(this.requests[0].url, 'dash.mpd', 'requested the manifest');
});

QUnit.test('redirect manifest request when handleManifestRedirects is true', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls, { handleManifestRedirects: true });

loader.load();

let modifiedRequest = this.requests.shift();

modifiedRequest.responseURL = 'http://differenturi.com/test.mpd';

this.standardXHRResponse(modifiedRequest);

assert.equal(loader.srcUrl, 'http://differenturi.com/test.mpd', 'url has redirected');
});

QUnit.test('redirect src request when handleManifestRedirects is true', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls, { handleManifestRedirects: true });

loader.load();

let modifiedRequest = this.requests.shift();

modifiedRequest.responseURL = 'http://differenturi.com/test.mpd';
this.standardXHRResponse(modifiedRequest);

let childLoader = new DashPlaylistLoader(loader.master.playlists['placeholder-uri-0'], this.fakeHls, false, loader);

childLoader.load();
this.clock.tick(1);

assert.equal(childLoader.media_.resolvedUri, 'http://differenturi.com/placeholder-uri-0', 'url has redirected');
});

QUnit.test('do not redirect src request when handleManifestRedirects is not set', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);

loader.load();

let modifiedRequest = this.requests.shift();

modifiedRequest.responseURL = 'http://differenturi.com/test.mpd';

this.standardXHRResponse(modifiedRequest);

assert.equal(loader.srcUrl, 'dash.mpd', 'url has not redirected');
});

QUnit.test('starts without any metadata', function(assert) {
let loader = new DashPlaylistLoader('dash.mpd', this.fakeHls);

Expand Down
20 changes: 20 additions & 0 deletions test/master-playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ QUnit.test('obeys auto preload option', function(assert) {
assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
});

QUnit.test('passes options to PlaylistLoader', function(assert) {
const options = {
url: 'test',
tech: this.player.tech_
};

let controller = new MasterPlaylistController(options);

assert.notOk(controller.masterPlaylistLoader_.withCredentials, 'credentials wont be sent by default');
assert.notOk(controller.masterPlaylistLoader_.handleManifestRedirects, 'redirects are ignored by default');

controller = new MasterPlaylistController(Object.assign({
withCredentials: true,
handleManifestRedirects: true
}, options));

assert.ok(controller.masterPlaylistLoader_.withCredentials, 'withCredentials enabled');
assert.ok(controller.masterPlaylistLoader_.handleManifestRedirects, 'handleManifestRedirects enabled');
});

QUnit.test('obeys metadata preload option', function(assert) {
this.player.preload('metadata');
// master
Expand Down
1 change: 0 additions & 1 deletion test/playback.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ QUnit.test('Live DASH', function(assert) {

QUnit.test('loops', function(assert) {
assert.timeout(5000);

let done = assert.async();
let player = this.player;

Expand Down
Loading

0 comments on commit b820f24

Please sign in to comment.