From e74ad98eaad82fb60c4940a0ad4e661f8679e90a Mon Sep 17 00:00:00 2001 From: Jacob Trimble Date: Wed, 19 Aug 2015 16:55:49 -0700 Subject: [PATCH] Added setNetworkCallback method to dash and offline video sources. This allows the app to intercept media requests to modify its URLs. The callback accepts the URL for the request and returns a modified URL or null to use the original. Closes #148 Change-Id: I08352754ace05f318706fd93910097c0fa7696f0 --- lib/dash/container_segment_index_source.js | 13 +++- lib/dash/duration_segment_index_source.js | 11 ++- lib/dash/dynamic_live_segment_index.js | 12 +++- lib/dash/list_segment_index_source.js | 10 ++- lib/dash/mpd_processor.js | 80 ++++++++++++++-------- lib/dash/mpd_utils.js | 14 ++-- lib/dash/timeline_segment_index_source.js | 10 ++- lib/media/mp4_segment_index_parser.js | 11 +-- lib/media/offline_segment_index_source.js | 1 + lib/media/webm_segment_index_parser.js | 19 +++-- lib/player/dash_video_source.js | 21 +++++- lib/player/offline_video_source.js | 23 ++++++- lib/util/failover_uri.js | 29 ++++++-- lib/util/license_request.js | 2 +- spec/ajax_request_spec.js | 2 +- spec/content_database_spec.js | 2 +- spec/player_integration.js | 57 +++++++++++++++ spec/util.js | 2 +- third_party/closure/goog/uri/uri.js | 4 ++ 19 files changed, 250 insertions(+), 73 deletions(-) diff --git a/lib/dash/container_segment_index_source.js b/lib/dash/container_segment_index_source.js index 93de3d2025..2f17c6cd8b 100644 --- a/lib/dash/container_segment_index_source.js +++ b/lib/dash/container_segment_index_source.js @@ -46,13 +46,14 @@ goog.require('shaka.util.TypedBind'); * for MP4 containers. * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @constructor * @struct * @implements {shaka.media.ISegmentIndexSource} */ shaka.dash.ContainerSegmentIndexSource = function( mpd, period, containerType, indexMetadata, initMetadata, - manifestCreationTime) { + manifestCreationTime, networkCallback) { shaka.asserts.assert(containerType != 'webm' || initMetadata); /** @private {!shaka.dash.mpd.Mpd} */ @@ -78,6 +79,9 @@ shaka.dash.ContainerSegmentIndexSource = function( /** @private {shaka.media.SegmentIndex} */ this.segmentIndex_ = null; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.networkCallback_ = networkCallback; }; @@ -88,6 +92,7 @@ shaka.dash.ContainerSegmentIndexSource = function( shaka.dash.ContainerSegmentIndexSource.prototype.destroy = function() { this.mpd_ = null; this.period_ = null; + this.networkCallback_ = null; this.indexMetadata_.abortFetch(); this.indexMetadata_ = null; @@ -128,13 +133,15 @@ shaka.dash.ContainerSegmentIndexSource.prototype.create = function() { var parser = new shaka.media.Mp4SegmentIndexParser(); references = parser.parse(new DataView(indexData), this.indexMetadata_.startByte, - this.indexMetadata_.urls); + this.indexMetadata_.urls, + this.networkCallback_); } else if (this.containerType_ == 'webm') { shaka.asserts.assert(initData); var parser = new shaka.media.WebmSegmentIndexParser(); references = parser.parse(new DataView(indexData), new DataView(initData), - this.indexMetadata_.urls); + this.indexMetadata_.urls, + this.networkCallback_); } else { shaka.asserts.unreachable(); } diff --git a/lib/dash/duration_segment_index_source.js b/lib/dash/duration_segment_index_source.js index 0d2dbe8d53..fef0a4dbe0 100644 --- a/lib/dash/duration_segment_index_source.js +++ b/lib/dash/duration_segment_index_source.js @@ -38,12 +38,13 @@ goog.require('shaka.util.TypedBind'); * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @constructor * @struct * @implements {shaka.media.ISegmentIndexSource} */ shaka.dash.DurationSegmentIndexSource = function( - mpd, period, representation, manifestCreationTime) { + mpd, period, representation, manifestCreationTime, networkCallback) { shaka.asserts.assert(period.start != null); shaka.asserts.assert((mpd.type == 'dynamic') || (period.duration != null)); shaka.asserts.assert(representation.segmentTemplate); @@ -64,6 +65,9 @@ shaka.dash.DurationSegmentIndexSource = function( /** @private {shaka.media.SegmentIndex} */ this.segmentIndex_ = null; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.networkCallback_ = networkCallback; }; @@ -75,6 +79,7 @@ shaka.dash.DurationSegmentIndexSource.prototype.destroy = function() { this.mpd_ = null; this.period_ = null; this.representation_ = null; + this.networkCallback_ = null; if (this.segmentIndex_) { this.segmentIndex_.destroy(); @@ -93,7 +98,7 @@ shaka.dash.DurationSegmentIndexSource.prototype.create = function() { try { this.segmentIndex_ = new shaka.dash.DynamicLiveSegmentIndex( this.mpd_, this.period_, this.representation_, - this.manifestCreationTime_); + this.manifestCreationTime_, this.networkCallback_); } catch (exception) { return Promise.reject(exception); } @@ -103,7 +108,7 @@ shaka.dash.DurationSegmentIndexSource.prototype.create = function() { segmentTemplate.segmentDuration / segmentTemplate.timescale; var numSegments = Math.ceil(this.period_.duration / scaledSegmentDuration); var references = shaka.dash.MpdUtils.generateSegmentReferences( - this.representation_, 1, numSegments); + this.networkCallback_, this.representation_, 1, numSegments); if (!references) { var error = new Error('Failed to generate SegmentReferences'); error.type = 'stream'; diff --git a/lib/dash/dynamic_live_segment_index.js b/lib/dash/dynamic_live_segment_index.js index 163dd36300..2c9a6049c0 100644 --- a/lib/dash/dynamic_live_segment_index.js +++ b/lib/dash/dynamic_live_segment_index.js @@ -43,6 +43,7 @@ goog.require('shaka.util.ArrayUtils'); * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @throws {Error} If the SegmentIndex's corresponding stream is available but * the initial SegmentReferences could not be generated. * @constructor @@ -50,7 +51,7 @@ goog.require('shaka.util.ArrayUtils'); * @extends {shaka.dash.LiveSegmentIndex} */ shaka.dash.DynamicLiveSegmentIndex = function( - mpd, period, representation, manifestCreationTime) { + mpd, period, representation, manifestCreationTime, networkCallback) { shaka.asserts.assert(mpd.availabilityStartTime != null); shaka.asserts.assert(period.start != null); shaka.asserts.assert(representation.segmentTemplate); @@ -70,7 +71,7 @@ shaka.dash.DynamicLiveSegmentIndex = function( } var references = shaka.dash.MpdUtils.generateSegmentReferences( - representation, earliestSegmentNumber, numSegments); + networkCallback, representation, earliestSegmentNumber, numSegments); if (references == null) { var error = new Error('Failed to generate SegmentReferences.'); error.type = 'stream'; @@ -114,6 +115,9 @@ shaka.dash.DynamicLiveSegmentIndex = function( * @private {?number} */ this.nextSegmentNumber_ = pair ? pair.current + 1 : null; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.networkCallback_ = networkCallback; }; goog.inherits(shaka.dash.DynamicLiveSegmentIndex, shaka.dash.LiveSegmentIndex); @@ -295,6 +299,7 @@ shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ = */ shaka.dash.DynamicLiveSegmentIndex.prototype.destroy = function() { this.representation_ = null; + this.networkCallback_ = null; shaka.dash.LiveSegmentIndex.prototype.destroy.call(this); }; @@ -410,7 +415,8 @@ shaka.dash.DynamicLiveSegmentIndex.prototype.generateSegmentReferences_ = // Generate and correct the new SegmentReferences. var newReferences = shaka.dash.MpdUtils.generateSegmentReferences( - this.representation_, this.nextSegmentNumber_, numNewSegments); + this.networkCallback_, this.representation_, + this.nextSegmentNumber_, numNewSegments); // |newReferences| should never be null since generateSegmentReferences() // should have been called at least once successfully with |representation_|. diff --git a/lib/dash/list_segment_index_source.js b/lib/dash/list_segment_index_source.js index 529eccbf08..826a8a1400 100644 --- a/lib/dash/list_segment_index_source.js +++ b/lib/dash/list_segment_index_source.js @@ -37,12 +37,13 @@ goog.require('shaka.util.TypedBind'); * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @constructor * @struct * @implements {shaka.media.ISegmentIndexSource} */ shaka.dash.ListSegmentIndexSource = function( - mpd, period, representation, manifestCreationTime) { + mpd, period, representation, manifestCreationTime, networkCallback) { shaka.asserts.assert(representation.segmentList); // Alias @@ -67,6 +68,9 @@ shaka.dash.ListSegmentIndexSource = function( /** @private {number} */ this.manifestCreationTime_ = manifestCreationTime; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.networkCallback_ = networkCallback; }; @@ -78,6 +82,7 @@ shaka.dash.ListSegmentIndexSource.prototype.destroy = function() { this.mpd_ = null; this.period_ = null; this.representation_ = null; + this.networkCallback_ = null; if (this.segmentIndex_) { this.segmentIndex_.destroy(); @@ -164,7 +169,8 @@ shaka.dash.ListSegmentIndexSource.prototype.create = function() { scaledStartTime, scaledEndTime, new shaka.util.FailoverUri( - segmentUrl.mediaUrl, startByte, endByte))); + this.networkCallback_, segmentUrl.mediaUrl, + startByte, endByte))); } this.segmentIndex_ = this.mpd_.type == 'dynamic' ? diff --git a/lib/dash/mpd_processor.js b/lib/dash/mpd_processor.js index ed5d3f0c1c..2bbfee697a 100644 --- a/lib/dash/mpd_processor.js +++ b/lib/dash/mpd_processor.js @@ -66,9 +66,10 @@ shaka.dash.MpdProcessor.DEFAULT_MIN_BUFFER_TIME = 5.0; * This function modifies |mpd| but does not take ownership of it. * * @param {!shaka.dash.mpd.Mpd} mpd + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {!shaka.media.ManifestInfo} */ -shaka.dash.MpdProcessor.prototype.process = function(mpd) { +shaka.dash.MpdProcessor.prototype.process = function(mpd, networkCallback) { var manifestCreationTime = shaka.util.Clock.now() / 1000.0; this.validateSegmentInfo_(mpd); @@ -83,7 +84,7 @@ shaka.dash.MpdProcessor.prototype.process = function(mpd) { mpd.availabilityStartTime = manifestCreationTime; } - return this.createManifestInfo_(mpd, manifestCreationTime); + return this.createManifestInfo_(mpd, manifestCreationTime, networkCallback); }; @@ -315,11 +316,12 @@ shaka.dash.MpdProcessor.prototype.filterAdaptationSet_ = function( * @param {!shaka.dash.mpd.Mpd} mpd * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {!shaka.media.ManifestInfo} * @private */ shaka.dash.MpdProcessor.prototype.createManifestInfo_ = function( - mpd, manifestCreationTime) { + mpd, manifestCreationTime, networkCallback) { var manifestInfo = new shaka.media.ManifestInfo(); if (mpd.type == 'dynamic') { @@ -328,7 +330,7 @@ shaka.dash.MpdProcessor.prototype.createManifestInfo_ = function( // Prefer the URL specified by the Location element. manifestInfo.updateUrl = new shaka.util.FailoverUri( - mpd.updateLocation || mpd.url); + null, mpd.updateLocation || mpd.url); } manifestInfo.minBufferTime = mpd.minBufferTime || @@ -379,7 +381,7 @@ shaka.dash.MpdProcessor.prototype.createManifestInfo_ = function( } var streamInfo = this.createStreamInfo_( - mpd, period, representation, manifestCreationTime); + mpd, period, representation, manifestCreationTime, networkCallback); if (!streamInfo) { // An error has already been logged. continue; @@ -477,12 +479,13 @@ shaka.dash.MpdProcessor.prototype.getDrmSchemeInfos_ = * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {shaka.media.StreamInfo} The new StreamInfo on success; otherwise, * return null. * @private */ shaka.dash.MpdProcessor.prototype.createStreamInfo_ = function( - mpd, period, representation, manifestCreationTime) { + mpd, period, representation, manifestCreationTime, networkCallback) { if (!representation.baseUrl || representation.baseUrl.length === 0) { shaka.log.warning( 'Representation does not contain sufficient segment information:', @@ -497,24 +500,24 @@ shaka.dash.MpdProcessor.prototype.createStreamInfo_ = function( if (representation.segmentBase) { streamInfo = this.createStreamInfoFromSegmentBase_( - mpd, period, representation, manifestCreationTime); + mpd, period, representation, manifestCreationTime, networkCallback); timescale = representation.segmentBase.timescale; presentationTimeOffset = representation.segmentBase.presentationTimeOffset; } else if (representation.segmentList) { streamInfo = this.createStreamInfoFromSegmentList_( - mpd, period, representation, manifestCreationTime); + mpd, period, representation, manifestCreationTime, networkCallback); timescale = representation.segmentList.timescale; presentationTimeOffset = representation.segmentList.presentationTimeOffset; } else if (representation.segmentTemplate) { streamInfo = this.createStreamInfoFromSegmentTemplate_( - mpd, period, representation, manifestCreationTime); + mpd, period, representation, manifestCreationTime, networkCallback); timescale = representation.segmentTemplate.timescale; presentationTimeOffset = representation.segmentTemplate.presentationTimeOffset; } else if (representation.mimeType.split('/')[0] == 'text') { streamInfo = new shaka.media.StreamInfo(); streamInfo.segmentIndexSource = new shaka.media.TextSegmentIndexSource( - new shaka.util.FailoverUri(representation.baseUrl)); + new shaka.util.FailoverUri(networkCallback, representation.baseUrl)); } else { shaka.asserts.unreachable(); } @@ -553,12 +556,13 @@ shaka.dash.MpdProcessor.prototype.createStreamInfo_ = function( * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {shaka.media.StreamInfo} A streamInfo on success; otherwise, * return null. * @private */ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentBase_ = function( - mpd, period, representation, manifestCreationTime) { + mpd, period, representation, manifestCreationTime, networkCallback) { shaka.asserts.assert(representation.segmentBase); shaka.asserts.assert(representation.segmentBase.timescale > 0); @@ -605,10 +609,11 @@ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentBase_ = function( null; } - var indexMetadata = this.createSegmentMetadata_(representationIndex); + var indexMetadata = this.createSegmentMetadata_( + representationIndex, networkCallback); var initMetadata = segmentBase.initialization ? - this.createSegmentMetadata_(segmentBase.initialization) : + this.createSegmentMetadata_(segmentBase.initialization, networkCallback) : null; var segmentIndexSource = @@ -618,7 +623,8 @@ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentBase_ = function( containerType, indexMetadata, initMetadata, - manifestCreationTime); + manifestCreationTime, + networkCallback); var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata); var streamInfo = new shaka.media.StreamInfo(); @@ -637,12 +643,14 @@ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentBase_ = function( * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {shaka.media.StreamInfo} A StreamInfo on success; otherwise, * return null. * @private */ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentList_ = - function(mpd, period, representation, manifestCreationTime) { + function(mpd, period, representation, manifestCreationTime, + networkCallback) { shaka.asserts.assert(representation.segmentList); var segmentList = representation.segmentList; @@ -678,12 +686,12 @@ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentList_ = var initMetadata = segmentList.initialization ? - this.createSegmentMetadata_(segmentList.initialization) : + this.createSegmentMetadata_(segmentList.initialization, networkCallback) : null; var segmentIndexSource = new shaka.dash.ListSegmentIndexSource( - mpd, period, representation, manifestCreationTime); + mpd, period, representation, manifestCreationTime, networkCallback); var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata); var streamInfo = new shaka.media.StreamInfo(); @@ -702,12 +710,14 @@ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentList_ = * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {shaka.media.StreamInfo} A StreamInfo on success; otherwise, * return null. * @private */ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentTemplate_ = - function(mpd, period, representation, manifestCreationTime) { + function(mpd, period, representation, manifestCreationTime, + networkCallback) { shaka.asserts.assert(representation.segmentTemplate); var segmentTemplate = /** @type {!shaka.dash.mpd.SegmentTemplate} */ ( @@ -729,10 +739,12 @@ shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentTemplate_ = } var initMetadata = - initialization ? this.createSegmentMetadata_(initialization) : null; + initialization ? this.createSegmentMetadata_( + initialization, networkCallback) : null; var segmentIndexSource = this.makeSegmentIndexSourceViaSegmentTemplate_( - mpd, period, representation, manifestCreationTime, initMetadata); + mpd, period, representation, manifestCreationTime, initMetadata, + networkCallback); if (!segmentIndexSource) { // An error has already been logged. return null; @@ -801,19 +813,22 @@ shaka.dash.MpdProcessor.prototype.validateSegmentTemplate_ = function( * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. * @param {shaka.util.FailoverUri} initMetadata + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {shaka.media.ISegmentIndexSource} A SegmentIndexSource on success; * otherwise, return null. * @private */ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaSegmentTemplate_ = - function(mpd, period, representation, manifestCreationTime, initMetadata) { + function(mpd, period, representation, manifestCreationTime, initMetadata, + networkCallback) { shaka.asserts.assert(representation.segmentTemplate); var segmentTemplate = representation.segmentTemplate; if (segmentTemplate.indexUrlTemplate) { return this.makeSegmentIndexSourceViaIndexUrlTemplate_( - mpd, period, representation, manifestCreationTime, initMetadata); + mpd, period, representation, manifestCreationTime, initMetadata, + networkCallback); } if (!segmentTemplate.mediaUrlTemplate) { @@ -826,7 +841,7 @@ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaSegmentTemplate_ = if (segmentTemplate.timeline) { return new shaka.dash.TimelineSegmentIndexSource( - mpd, period, representation, manifestCreationTime); + mpd, period, representation, manifestCreationTime, networkCallback); } else if (segmentTemplate.segmentDuration) { if ((mpd.type != 'dynamic') && (period.duration == null)) { shaka.log.warning( @@ -836,7 +851,7 @@ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaSegmentTemplate_ = return null; } return new shaka.dash.DurationSegmentIndexSource( - mpd, period, representation, manifestCreationTime); + mpd, period, representation, manifestCreationTime, networkCallback); } shaka.asserts.unreachable(); @@ -853,12 +868,14 @@ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaSegmentTemplate_ = * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. * @param {shaka.util.FailoverUri} initMetadata + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {shaka.media.ISegmentIndexSource} A SegmentIndexSource on success; * otherwise, return null. * @private */ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaIndexUrlTemplate_ = - function(mpd, period, representation, manifestCreationTime, initMetadata) { + function(mpd, period, representation, manifestCreationTime, initMetadata, + networkCallback) { shaka.asserts.assert(representation.segmentTemplate); shaka.asserts.assert(representation.segmentTemplate.indexUrlTemplate); @@ -884,7 +901,7 @@ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaIndexUrlTemplate_ = // Generate the media URL. var mediaUrl = shaka.dash.MpdUtils.createFromTemplate( - representation, 1, 0, 0, null); + networkCallback, representation, 1, 0, 0, null); if (!mediaUrl) { // An error has already been logged. return null; @@ -897,7 +914,8 @@ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaIndexUrlTemplate_ = return null; } - var indexMetadata = this.createSegmentMetadata_(representationIndex); + var indexMetadata = this.createSegmentMetadata_( + representationIndex, networkCallback); var segmentIndexSource = new shaka.dash.ContainerSegmentIndexSource( @@ -906,7 +924,8 @@ shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaIndexUrlTemplate_ = containerType, indexMetadata, initMetadata, - manifestCreationTime); + manifestCreationTime, + networkCallback); return segmentIndexSource; }; @@ -995,11 +1014,12 @@ shaka.dash.MpdProcessor.prototype.generateUrlTypeObject_ = function( * * @param {!shaka.dash.mpd.RepresentationIndex| * !shaka.dash.mpd.Initialization} urlTypeObject + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {!shaka.util.FailoverUri} * @private */ shaka.dash.MpdProcessor.prototype.createSegmentMetadata_ = function( - urlTypeObject) { + urlTypeObject, networkCallback) { var url = urlTypeObject.url; var startByte = 0; @@ -1009,6 +1029,6 @@ shaka.dash.MpdProcessor.prototype.createSegmentMetadata_ = function( endByte = urlTypeObject.range.end; } - return new shaka.util.FailoverUri(url, startByte, endByte); + return new shaka.util.FailoverUri(networkCallback, url, startByte, endByte); }; diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 285ff0c166..a4deeae3a5 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -42,6 +42,7 @@ shaka.dash.MpdUtils.GAP_OVERLAP_WARN_THRESHOLD = 1.0 / 32.0; * Generates a set of SegmentReferences from a SegmentTemplate with a 'duration' * attribute. * + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @param {!shaka.dash.mpd.Representation} representation * @param {number} firstSegmentNumber The segment number (one-based) of the * first SegmentReference to generate, relative to the start of the @@ -51,7 +52,7 @@ shaka.dash.MpdUtils.GAP_OVERLAP_WARN_THRESHOLD = 1.0 / 32.0; * success; otherwise, null. */ shaka.dash.MpdUtils.generateSegmentReferences = function( - representation, firstSegmentNumber, numSegments) { + networkCallback, representation, firstSegmentNumber, numSegments) { shaka.asserts.assert(representation.segmentTemplate); shaka.asserts.assert(representation.segmentTemplate.timescale > 0); shaka.asserts.assert(representation.segmentTemplate.segmentDuration); @@ -80,7 +81,7 @@ shaka.dash.MpdUtils.generateSegmentReferences = function( // Generate the media URL. var mediaUrl = shaka.dash.MpdUtils.createFromTemplate( - representation, segmentReplacement, timeReplacement, + networkCallback, representation, segmentReplacement, timeReplacement, 0 /* startByte */, null /* endByte */); if (!mediaUrl) { // An error has already been logged. @@ -101,6 +102,7 @@ shaka.dash.MpdUtils.generateSegmentReferences = function( /** * Creates a FailoverUri from a relative template URL. * + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @param {!shaka.dash.mpd.Representation} representation * @param {number} number * @param {number} time @@ -109,7 +111,7 @@ shaka.dash.MpdUtils.generateSegmentReferences = function( * @return {shaka.util.FailoverUri} */ shaka.dash.MpdUtils.createFromTemplate = function( - representation, number, time, startByte, endByte) { + networkCallback, representation, number, time, startByte, endByte) { shaka.asserts.assert(representation.segmentTemplate); if (!representation.segmentTemplate) return null; @@ -120,7 +122,8 @@ shaka.dash.MpdUtils.createFromTemplate = function( 'using the Representation\'s BaseURL instead.', representation); return representation.baseUrl ? - new shaka.util.FailoverUri(representation.baseUrl, startByte, endByte) : + new shaka.util.FailoverUri( + networkCallback, representation.baseUrl, startByte, endByte) : null; } @@ -138,7 +141,8 @@ shaka.dash.MpdUtils.createFromTemplate = function( var mediaUrl = shaka.util.FailoverUri.resolve( representation.baseUrl, filledUrlTemplate); - return new shaka.util.FailoverUri(mediaUrl, startByte, endByte); + return new shaka.util.FailoverUri( + networkCallback, mediaUrl, startByte, endByte); }; diff --git a/lib/dash/timeline_segment_index_source.js b/lib/dash/timeline_segment_index_source.js index fe1659e8a8..017cd305aa 100644 --- a/lib/dash/timeline_segment_index_source.js +++ b/lib/dash/timeline_segment_index_source.js @@ -37,12 +37,13 @@ goog.require('shaka.util.TypedBind'); * @param {!shaka.dash.mpd.Representation} representation * @param {number} manifestCreationTime The time, in seconds, when the manifest * was created. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @constructor * @struct * @implements {shaka.media.ISegmentIndexSource} */ shaka.dash.TimelineSegmentIndexSource = function( - mpd, period, representation, manifestCreationTime) { + mpd, period, representation, manifestCreationTime, networkCallback) { shaka.asserts.assert(period.start != null); shaka.asserts.assert(representation.segmentTemplate); shaka.asserts.assert(representation.segmentTemplate.mediaUrlTemplate); @@ -63,6 +64,9 @@ shaka.dash.TimelineSegmentIndexSource = function( /** @private {shaka.media.SegmentIndex} */ this.segmentIndex_ = null; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.networkCallback_ = networkCallback; }; @@ -74,6 +78,7 @@ shaka.dash.TimelineSegmentIndexSource.prototype.destroy = function() { this.mpd_ = null; this.period_ = null; this.representation_ = null; + this.networkCallback_ = null; if (this.segmentIndex_) { this.segmentIndex_.destroy(); @@ -116,7 +121,8 @@ shaka.dash.TimelineSegmentIndexSource.prototype.create = function() { // Generate the media URL. var mediaUrl = shaka.dash.MpdUtils.createFromTemplate( - this.representation_, segmentReplacement, timeReplacement, 0, null); + this.networkCallback_, this.representation_, segmentReplacement, + timeReplacement, 0, null); if (!mediaUrl) { var error = new Error('Failed to generate media URL.'); error.type = 'dash'; diff --git a/lib/media/mp4_segment_index_parser.js b/lib/media/mp4_segment_index_parser.js index ace04eeaa7..89a338b39e 100644 --- a/lib/media/mp4_segment_index_parser.js +++ b/lib/media/mp4_segment_index_parser.js @@ -42,15 +42,17 @@ shaka.media.Mp4SegmentIndexParser = function() {}; * @param {number} sidxOffset The SIDX's offset, in bytes, from the start of * the MP4 container. * @param {!Array.} url The location of each SegmentReference. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {Array.} SegmentReferences on success; * otherwise, return null. */ shaka.media.Mp4SegmentIndexParser.prototype.parse = function( - sidxData, sidxOffset, url) { + sidxData, sidxOffset, url, networkCallback) { var references = null; try { - references = this.parseInternal_(sidxData, sidxOffset, url); + references = this.parseInternal_( + sidxData, sidxOffset, url, networkCallback); } catch (exception) { if (!(exception instanceof RangeError)) { throw exception; @@ -74,6 +76,7 @@ shaka.media.Mp4SegmentIndexParser.SIDX_INDICATOR = 0x73696478; * @param {!DataView} sidxData * @param {number} sidxOffset * @param {!Array.} url + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {Array.} SegmentReferences on success; * otherwise, return null. * @throws {RangeError} @@ -81,7 +84,7 @@ shaka.media.Mp4SegmentIndexParser.SIDX_INDICATOR = 0x73696478; * @see ISO/IEC 14496-12:2012 section 4.2 and 8.16.3 */ shaka.media.Mp4SegmentIndexParser.prototype.parseInternal_ = function( - sidxData, sidxOffset, url) { + sidxData, sidxOffset, url, networkCallback) { var references = []; var reader = new shaka.util.DataViewReader( @@ -164,7 +167,7 @@ shaka.media.Mp4SegmentIndexParser.prototype.parseInternal_ = function( } var failover = new shaka.util.FailoverUri( - url, startByte, startByte + referenceSize - 1); + networkCallback, url, startByte, startByte + referenceSize - 1); references.push( new shaka.media.SegmentReference( unscaledStartTime / timescale, diff --git a/lib/media/offline_segment_index_source.js b/lib/media/offline_segment_index_source.js index 69150ac045..61da20bfa0 100644 --- a/lib/media/offline_segment_index_source.js +++ b/lib/media/offline_segment_index_source.js @@ -74,6 +74,7 @@ shaka.media.OfflineSegmentIndexSource.prototype.create = function() { info['start_time'], info['end_time'], new shaka.util.FailoverUri( + null, [new goog.Uri(info['url'])], info['start_byte'], null /* endByte */))); diff --git a/lib/media/webm_segment_index_parser.js b/lib/media/webm_segment_index_parser.js index e7941d88fb..6c448fb565 100644 --- a/lib/media/webm_segment_index_parser.js +++ b/lib/media/webm_segment_index_parser.js @@ -43,15 +43,16 @@ shaka.media.WebmSegmentIndexParser = function() {}; * @param {!DataView} cuesData The WebM container's "Cueing Data" section. * @param {!DataView} initData The WebM container's headers. * @param {!Array.} url The location of each SegmentReference. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {Array.} SegmentReferences on success; * otherwise, return null. */ shaka.media.WebmSegmentIndexParser.prototype.parse = function( - cuesData, initData, url) { + cuesData, initData, url, networkCallback) { var references = null; try { - references = this.parseInternal_(cuesData, initData, url); + references = this.parseInternal_(cuesData, initData, url, networkCallback); } catch (exception) { if (!(exception instanceof RangeError)) { throw exception; @@ -105,13 +106,14 @@ shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1; * @return {Array.} SegmentReferences on success; * otherwise, return null. * @param {!Array.} url The location of each SegmentReference. + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @throws {RangeError} * @see http://www.matroska.org/technical/specs/index.html * @see http://www.webmproject.org/docs/container/ * @private */ shaka.media.WebmSegmentIndexParser.prototype.parseInternal_ = function( - cuesData, initData, url) { + cuesData, initData, url, networkCallback) { var tuple = this.parseWebmContainer_(initData); if (!tuple) { return null; @@ -125,7 +127,8 @@ shaka.media.WebmSegmentIndexParser.prototype.parseInternal_ = function( } return this.parseCues_( - cuesElement, tuple.segmentOffset, tuple.timecodeScale, url); + cuesElement, tuple.segmentOffset, tuple.timecodeScale, url, + networkCallback); }; @@ -239,11 +242,12 @@ shaka.media.WebmSegmentIndexParser.prototype.parseInfo_ = function( * @param {number} segmentOffset * @param {number} timecodeScale * @param {!Array.} url + * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback * @return {!Array.} * @private */ shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function( - cuesElement, segmentOffset, timecodeScale, url) { + cuesElement, segmentOffset, timecodeScale, url, networkCallback) { var references = []; var parser = cuesElement.createParser(); @@ -269,7 +273,7 @@ shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function( shaka.asserts.assert(lastOffset >= 0); var failover = new shaka.util.FailoverUri( - url, lastOffset, currentOffset - 1); + networkCallback, url, lastOffset, currentOffset - 1); references.push( new shaka.media.SegmentReference(lastTime, currentTime, failover)); } @@ -281,7 +285,8 @@ shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function( if (lastTime >= 0) { shaka.asserts.assert(lastOffset >= 0); - var failover = new shaka.util.FailoverUri(url, lastOffset, null); + var failover = new shaka.util.FailoverUri(networkCallback, url, + lastOffset, null); references.push( new shaka.media.SegmentReference(lastTime, null, failover)); } diff --git a/lib/player/dash_video_source.js b/lib/player/dash_video_source.js index 8e75f629b2..ac620738b8 100644 --- a/lib/player/dash_video_source.js +++ b/lib/player/dash_video_source.js @@ -62,7 +62,7 @@ shaka.player.DashVideoSource = shaka.player.StreamVideoSource.call(this, null, estimator, abrManager); /** @private {!shaka.util.FailoverUri} */ - this.mpdUrl_ = new shaka.util.FailoverUri([new goog.Uri(mpdUrl)]); + this.mpdUrl_ = new shaka.util.FailoverUri(null, [new goog.Uri(mpdUrl)]); /** @private {?shaka.player.DashVideoSource.ContentProtectionCallback} */ this.interpretContentProtection_ = interpretContentProtection; @@ -75,6 +75,9 @@ shaka.player.DashVideoSource = /** @private {!Array.} */ this.captionsMime_ = []; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.networkCallback_ = null; }; goog.inherits(shaka.player.DashVideoSource, shaka.player.StreamVideoSource); if (shaka.features.Dash) { @@ -118,9 +121,21 @@ shaka.player.DashVideoSource.prototype.addExternalCaptions = }; +/** + * Sets the callback used to modify each segment request's URL and headers. + * + * @param {!shaka.util.FailoverUri.NetworkCallback} callback + * @export + */ +shaka.player.DashVideoSource.prototype.setNetworkCallback = function(callback) { + this.networkCallback_ = callback; +}; + + /** @override */ shaka.player.DashVideoSource.prototype.destroy = function() { this.interpretContentProtection_ = null; + this.networkCallback_ = null; shaka.player.StreamVideoSource.prototype.destroy.call(this); }; @@ -139,7 +154,7 @@ shaka.player.DashVideoSource.prototype.load = function() { var mpdProcessor = new shaka.dash.MpdProcessor(this.interpretContentProtection_); - this.manifestInfo = mpdProcessor.process(mpd); + this.manifestInfo = mpdProcessor.process(mpd, this.networkCallback_); var baseClassLoad = shaka.player.StreamVideoSource.prototype.load; var p = baseClassLoad.call(this); @@ -159,7 +174,7 @@ shaka.player.DashVideoSource.prototype.onUpdateManifest = function(url) { function(mpd) { var mpdProcessor = new shaka.dash.MpdProcessor(this.interpretContentProtection_); - var newManifestInfo = mpdProcessor.process(mpd); + var newManifestInfo = mpdProcessor.process(mpd, this.networkCallback_); return Promise.resolve(newManifestInfo); }) ); diff --git a/lib/player/offline_video_source.js b/lib/player/offline_video_source.js index f52b7aeac9..498ef4bd24 100644 --- a/lib/player/offline_video_source.js +++ b/lib/player/offline_video_source.js @@ -81,6 +81,9 @@ shaka.player.OfflineVideoSource = function(groupId, estimator, abrManager) { /** @private {!Object.} */ this.config_ = {}; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.networkCallback_ = null; }; goog.inherits(shaka.player.OfflineVideoSource, shaka.player.StreamVideoSource); if (shaka.features.Offline) { @@ -182,7 +185,8 @@ shaka.player.OfflineVideoSource.prototype.store = function( /** @type {!Array.} */ var selectedStreams = []; - var failover = new shaka.util.FailoverUri([new goog.Uri(mpdUrl)]); + var failover = new shaka.util.FailoverUri(this.networkCallback_, + [new goog.Uri(mpdUrl)]); var mpdRequest = new shaka.dash.MpdRequest(failover, this.mpdRequestTimeout); return mpdRequest.send().then(shaka.util.TypedBind(this, @@ -190,7 +194,7 @@ shaka.player.OfflineVideoSource.prototype.store = function( function(mpd) { var mpdProcessor = new shaka.dash.MpdProcessor(interpretContentProtection); - this.manifestInfo = mpdProcessor.process(mpd); + this.manifestInfo = mpdProcessor.process(mpd, this.networkCallback_); if (this.manifestInfo.live) { var error = new Error('Unable to store live streams offline.'); @@ -294,6 +298,18 @@ if (shaka.features.Dash) { } +/** + * Sets the callback used to intercept the URL in network requests. + * + * @param {!shaka.util.FailoverUri.NetworkCallback} callback + * @export + */ +shaka.player.OfflineVideoSource.prototype.setNetworkCallback = + function(callback) { + this.networkCallback_ = callback; +}; + + /** * Creates sourceBuffers and appends init data for each of the given streams. * This should trigger encrypted events for any encrypted streams. @@ -465,7 +481,8 @@ shaka.player.OfflineVideoSource.prototype.reconstructManifestInfo_ = new goog.Uri('data:application/octet-stream;base64,' + shaka.util.Uint8ArrayUtils.toBase64(initData)); var segmentInitSource = new shaka.media.SegmentInitSource( - new shaka.util.FailoverUri([segmentInitUrl], 0, null)); + new shaka.util.FailoverUri(this.networkCallback_, [segmentInitUrl], + 0, null)); streamInfo.segmentIndexSource = segmentIndexSource; streamInfo.segmentInitSource = segmentInitSource; diff --git a/lib/util/failover_uri.js b/lib/util/failover_uri.js index 0bfa826922..e503a8d528 100644 --- a/lib/util/failover_uri.js +++ b/lib/util/failover_uri.js @@ -29,13 +29,14 @@ goog.require('shaka.util.AjaxRequest'); /** * Creates a FailoverUri. * + * @param {shaka.util.FailoverUri.NetworkCallback} callback * @param {Array.} urls * @param {number=} opt_startByte The start byte of the data, defaults to 0. * @param {?number=} opt_endByte The end byte of the data, null means the end; * defaults to null. * @constructor */ -shaka.util.FailoverUri = function(urls, opt_startByte, opt_endByte) { +shaka.util.FailoverUri = function(callback, urls, opt_startByte, opt_endByte) { shaka.asserts.assert(urls); shaka.asserts.assert(urls.length > 0); @@ -53,9 +54,24 @@ shaka.util.FailoverUri = function(urls, opt_startByte, opt_endByte) { /** @private {shaka.util.AjaxRequest} */ this.request_ = null; + + /** @private {shaka.util.FailoverUri.NetworkCallback} */ + this.callback_ = callback; }; +/** + * A callback to the application called prior to media network events. The + * first parameter is the URL for the request. The second parameter is the + * headers for the request. These can be modified. The callback should return + * a modified URL for the request, or null to use the original. + * + * @typedef {?function(string,!Object.):(string?)} + * @exportDoc + */ +shaka.util.FailoverUri.NetworkCallback; + + /** * Resolves a relative url to the given |baseUrl|. * @@ -118,15 +134,19 @@ shaka.util.FailoverUri.prototype.abortFetch = function() { * * @private * @param {number} i - * @param {shaka.util.AjaxRequest.Parameters} parameters + * @param {!shaka.util.AjaxRequest.Parameters} parameters * @param {shaka.util.IBandwidthEstimator=} opt_estimator * @return {!Promise.} */ shaka.util.FailoverUri.prototype.createRequest_ = function(i, parameters, opt_estimator) { shaka.asserts.assert(i < this.urls.length); - this.request_ = new shaka.util.AjaxRequest( - this.urls[i].toString(), parameters); + + var url = this.urls[i].toString(); + if (this.callback_) { + url = this.callback_(url, parameters.requestHeaders) || url; + } + this.request_ = new shaka.util.AjaxRequest(url, parameters); if (opt_estimator) { this.request_.estimator = opt_estimator; } @@ -164,6 +184,7 @@ shaka.util.FailoverUri.prototype.createRequest_ = */ shaka.util.FailoverUri.prototype.clone = function() { return new shaka.util.FailoverUri( + this.callback_, this.urls.map(function(a) { return a.clone(); }), this.startByte, this.endByte diff --git a/lib/util/license_request.js b/lib/util/license_request.js index a0d75002d2..ede11c73ae 100644 --- a/lib/util/license_request.js +++ b/lib/util/license_request.js @@ -53,7 +53,7 @@ shaka.util.LicenseRequest = function( shaka.asserts.assert((method == 'GET') || (method == 'POST')); /** @private {!shaka.util.FailoverUri} */ - this.url_ = new shaka.util.FailoverUri([new goog.Uri(url)]); + this.url_ = new shaka.util.FailoverUri(null, [new goog.Uri(url)]); /** @private {!shaka.util.AjaxRequest.Parameters} */ this.parameters_ = new shaka.util.AjaxRequest.Parameters(); diff --git a/spec/ajax_request_spec.js b/spec/ajax_request_spec.js index b37ae3f436..44b33055f0 100644 --- a/spec/ajax_request_spec.js +++ b/spec/ajax_request_spec.js @@ -35,7 +35,7 @@ describe('AjaxRequest', function() { // Set up mock FailoverUri. The mock FailoverUri is used to insert a // stream into the database. originalFailoverUri = shaka.util.FailoverUri; - var mockFailoverUri = function(url, startByte, endByte) { + var mockFailoverUri = function(callback, url, startByte, endByte) { return { fetch: function() { return Promise.resolve(new ArrayBuffer(bufferSize)); diff --git a/spec/content_database_spec.js b/spec/content_database_spec.js index aeb8b4e1ed..8f6a0709b5 100644 --- a/spec/content_database_spec.js +++ b/spec/content_database_spec.js @@ -69,7 +69,7 @@ describe('ContentDatabase', function() { // Set up mock FailoverUri. originalFailoverUri = shaka.util.FailoverUri; - var mockFailoverUri = function(url, startByte, endByte) { + var mockFailoverUri = function(callback, url, startByte, endByte) { return { fetch: function() { return Promise.resolve(new ArrayBuffer(768 * 1024)); diff --git a/spec/player_integration.js b/spec/player_integration.js index c54c65b6f7..3ac30bdd62 100644 --- a/spec/player_integration.js +++ b/spec/player_integration.js @@ -525,6 +525,63 @@ describe('Player', function() { }); }); + describe('setNetworkCallback', function() { + it('intercepts network calls', function(done) { + var callback = jasmine.createSpy('network').and.callFake( + function(url, parameters) { + expect(url).toBeTruthy(); + expect(parameters).toBeTruthy(); + return null; + }); + var source = newSource(plainManifest); + source.setNetworkCallback(callback); + + player.load(source).then(function() { + video.play(); + return waitForMovement(video, eventManager); + }).then(function() { + expect(callback.calls.any()).toBe(true); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + + it('changes urls', function(done) { + var callback = jasmine.createSpy('network').and.callFake( + function(url, parameters) { + expect(url).toBeTruthy(); + expect(parameters).toBeTruthy(); + return url.replace('example', 'appspot'); + }); + + // Load the angel manifest and edit the urls to point to an + // invalid location. + var url = new shaka.util.FailoverUri(null, + [new goog.Uri(languagesManifest)]); + var params = new shaka.util.AjaxRequest.Parameters(); + params.responseType = 'text'; + url.fetch(params).then(function(data) { + var dummy = data.replace('appspot', 'example'); + dummy = 'data:text/plain,' + window.encodeURIComponent(dummy); + + var source = newSource(dummy); + source.setNetworkCallback(callback); + return player.load(source); + }).then(function() { + video.play(); + return waitForMovement(video, eventManager); + }).then(function() { + expect(callback.calls.any()).toBe(true); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + }); + it('supports failover', function(done) { player.load(newSource(failoverManifest)).then(function() { video.play(); diff --git a/spec/util.js b/spec/util.js index 2ab6554018..7ac7725b7b 100644 --- a/spec/util.js +++ b/spec/util.js @@ -331,7 +331,7 @@ function checkReference(reference, url, startTime, endTime) { */ function createFailover(url, opt_start, opt_end) { return new shaka.util.FailoverUri( - [new goog.Uri(url)], opt_start || 0, opt_end || null); + null, [new goog.Uri(url)], opt_start || 0, opt_end || null); } diff --git a/third_party/closure/goog/uri/uri.js b/third_party/closure/goog/uri/uri.js index 9d445100c5..2bd616215a 100644 --- a/third_party/closure/goog/uri/uri.js +++ b/third_party/closure/goog/uri/uri.js @@ -222,6 +222,10 @@ goog.Uri.prototype.toString = function() { goog.Uri.prototype.resolve = function(relativeUri) { var absoluteUri = this.clone(); + if (absoluteUri.scheme_ === 'data') { + // Cannot have a relative URI to a data URI. + absoluteUri = new goog.Uri(); + } // we satisfy these conditions by looking for the first part of relativeUri // that is not blank and applying defaults to the rest