mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-16 16:16:40 +03:00
7c1e31d4e6
Now that jsdoc supports this, it will make future diffs cleaner. See #8819 and #1236.
363 lines
9.8 KiB
JavaScript
363 lines
9.8 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.util.ContentSteeringManager');
|
|
|
|
goog.require('goog.Uri');
|
|
goog.require('shaka.media.ManifestParser');
|
|
goog.require('shaka.net.NetworkingEngine');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.util.IDestroyable');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
goog.require('shaka.util.OperationManager');
|
|
goog.require('shaka.util.StringUtils');
|
|
goog.require('shaka.util.Timer');
|
|
|
|
|
|
/**
|
|
* Create a Content Steering manager.
|
|
*
|
|
* @implements {shaka.util.IDestroyable}
|
|
*/
|
|
shaka.util.ContentSteeringManager = class {
|
|
/**
|
|
* @param {shaka.extern.ManifestParser.PlayerInterface} playerInterface
|
|
*/
|
|
constructor(playerInterface) {
|
|
/** @private {?shaka.extern.ManifestConfiguration} */
|
|
this.config_ = null;
|
|
|
|
/** @private {?shaka.extern.ManifestParser.PlayerInterface} */
|
|
this.playerInterface_ = playerInterface;
|
|
|
|
/** @private {!shaka.util.OperationManager} */
|
|
this.operationManager_ = new shaka.util.OperationManager();
|
|
|
|
/** @private {!Array<string>} */
|
|
this.baseUris_ = [];
|
|
|
|
/** @private {?string} */
|
|
this.defaultPathwayId_ = null;
|
|
|
|
/** @private {!Array<string>} */
|
|
this.pathwayPriority_ = [];
|
|
|
|
/** @private {?string} */
|
|
this.lastPathwayUsed_ = null;
|
|
|
|
/** @private {!Array<shaka.util.ContentSteeringManager.PathwayClone>} */
|
|
this.pathwayClones_ = [];
|
|
|
|
/**
|
|
* Default to 5 minutes. Value in seconds.
|
|
*
|
|
* @private {number}
|
|
*/
|
|
this.lastTTL_ = 300;
|
|
|
|
/** @private {!Map<(string | number), !Map<string, string>>} */
|
|
this.locations_ = new Map();
|
|
|
|
/** @private {!Map<string, number>} */
|
|
this.bannedLocations_ = new Map();
|
|
|
|
/** @private {?shaka.util.Timer} */
|
|
this.updateTimer_ = null;
|
|
|
|
/** @private {string} */
|
|
this.manifestType_ = shaka.media.ManifestParser.UNKNOWN;
|
|
}
|
|
|
|
/**
|
|
* @param {shaka.extern.ManifestConfiguration} config
|
|
*/
|
|
configure(config) {
|
|
this.config_ = config;
|
|
}
|
|
|
|
|
|
/** @override */
|
|
destroy() {
|
|
this.config_ = null;
|
|
this.playerInterface_ = null;
|
|
this.baseUris_ = [];
|
|
this.defaultPathwayId_ = null;
|
|
this.pathwayPriority_ = [];
|
|
this.pathwayClones_ = [];
|
|
this.locations_.clear();
|
|
|
|
if (this.updateTimer_ != null) {
|
|
this.updateTimer_.stop();
|
|
this.updateTimer_ = null;
|
|
}
|
|
|
|
return this.operationManager_.destroy();
|
|
}
|
|
|
|
/**
|
|
* @param {string} manifestType
|
|
*/
|
|
setManifestType(manifestType) {
|
|
this.manifestType_ = manifestType;
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<string>} baseUris
|
|
*/
|
|
setBaseUris(baseUris) {
|
|
this.baseUris_ = baseUris;
|
|
}
|
|
|
|
/**
|
|
* @param {?string} defaultPathwayId
|
|
*/
|
|
setDefaultPathwayId(defaultPathwayId) {
|
|
this.defaultPathwayId_ = defaultPathwayId;
|
|
}
|
|
|
|
/**
|
|
* Request the Content Steering info.
|
|
*
|
|
* @param {string} uri
|
|
* @return {!Promise}
|
|
*/
|
|
async requestInfo(uri) {
|
|
const uris = shaka.util.ManifestParserUtils.resolveUris(
|
|
this.baseUris_, [this.addQueryParams_(uri)]);
|
|
const type = shaka.net.NetworkingEngine.RequestType.CONTENT_STEERING;
|
|
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
uris, this.config_.retryParameters);
|
|
|
|
const op = this.playerInterface_.networkingEngine.request(type, request);
|
|
this.operationManager_.manage(op);
|
|
try {
|
|
const response = await op.promise;
|
|
const str = shaka.util.StringUtils.fromUTF8(response.data);
|
|
const steeringManifest =
|
|
/** @type {shaka.util.ContentSteeringManager.SteeringManifest} */
|
|
(JSON.parse(str));
|
|
if (steeringManifest.VERSION == 1) {
|
|
this.processManifest_(steeringManifest, response.uri);
|
|
}
|
|
} catch (e) {
|
|
if (e && e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
|
|
return;
|
|
}
|
|
if (this.updateTimer_ != null) {
|
|
this.updateTimer_.stop();
|
|
this.updateTimer_ = null;
|
|
}
|
|
this.updateTimer_ = new shaka.util.Timer(() => {
|
|
this.requestInfo(uri);
|
|
});
|
|
this.updateTimer_.tickAfter(this.lastTTL_);
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
addQueryParams_(uri) {
|
|
if (!this.pathwayPriority_.length) {
|
|
return uri;
|
|
}
|
|
const finalUri = new goog.Uri(uri);
|
|
const currentPathwayID = this.lastPathwayUsed_ || this.pathwayPriority_[0];
|
|
const currentBandwidth =
|
|
Math.round(this.playerInterface_.getBandwidthEstimate());
|
|
const queryData = finalUri.getQueryData();
|
|
if (this.manifestType_ == shaka.media.ManifestParser.DASH) {
|
|
queryData.add('_DASH_pathway', currentPathwayID);
|
|
queryData.add('_DASH_throughput', String(currentBandwidth));
|
|
} else if (this.manifestType_ == shaka.media.ManifestParser.HLS) {
|
|
queryData.add('_HLS_pathway', currentPathwayID);
|
|
queryData.add('_HLS_throughput', String(currentBandwidth));
|
|
}
|
|
if (queryData.getCount()) {
|
|
finalUri.setQueryData(queryData);
|
|
}
|
|
return finalUri.toString();
|
|
}
|
|
|
|
/**
|
|
* @param {shaka.util.ContentSteeringManager.SteeringManifest} manifest
|
|
* @param {string} finalManifestUri
|
|
* @private
|
|
*/
|
|
processManifest_(manifest, finalManifestUri) {
|
|
if (this.updateTimer_ != null) {
|
|
this.updateTimer_.stop();
|
|
this.updateTimer_ = null;
|
|
}
|
|
const uri = manifest['RELOAD-URI'] || finalManifestUri;
|
|
this.updateTimer_ = new shaka.util.Timer(() => {
|
|
this.requestInfo(uri);
|
|
});
|
|
const newTTL = manifest['TTL'];
|
|
if (newTTL) {
|
|
this.lastTTL_ = newTTL;
|
|
}
|
|
this.updateTimer_.tickAfter(this.lastTTL_);
|
|
this.pathwayPriority_ = manifest['PATHWAY-PRIORITY'] || [];
|
|
this.pathwayClones_ = manifest['PATHWAY-CLONES'] || [];
|
|
}
|
|
|
|
/**
|
|
* Clear the previous locations added.
|
|
*/
|
|
clearPreviousLocations() {
|
|
this.locations_.clear();
|
|
}
|
|
|
|
/**
|
|
* @param {string|number} streamId
|
|
* @param {string} pathwayId
|
|
* @param {string} uri
|
|
*/
|
|
addLocation(streamId, pathwayId, uri) {
|
|
let streamLocations = this.locations_.get(streamId);
|
|
if (!streamLocations) {
|
|
streamLocations = new Map();
|
|
}
|
|
streamLocations.set(pathwayId, uri);
|
|
this.locations_.set(streamId, streamLocations);
|
|
}
|
|
|
|
/**
|
|
* @param {string} uri
|
|
*/
|
|
banLocation(uri) {
|
|
const bannedUntil = Date.now() + 60000;
|
|
this.bannedLocations_.set(uri, bannedUntil);
|
|
}
|
|
|
|
/**
|
|
* Get the base locations ordered according the priority.
|
|
*
|
|
* @param {string|number} streamId
|
|
* @param {boolean=} ignoreBaseUrls
|
|
* @return {!Array<string>}
|
|
*/
|
|
getLocations(streamId, ignoreBaseUrls = false) {
|
|
const streamLocations = this.locations_.get(streamId) || new Map();
|
|
/** @type {!Array<!{pathwayId: string, location: string}>} */
|
|
let locationsPathwayIdMap = [];
|
|
for (const pathwayId of this.pathwayPriority_) {
|
|
const location = streamLocations.get(pathwayId);
|
|
if (location) {
|
|
locationsPathwayIdMap.push({pathwayId, location});
|
|
} else {
|
|
const clone = this.pathwayClones_.find((c) => c.ID == pathwayId);
|
|
if (clone) {
|
|
const cloneLocation = streamLocations.get(clone['BASE-ID']);
|
|
if (cloneLocation) {
|
|
if (clone['URI-REPLACEMENT'].HOST) {
|
|
const uri = new goog.Uri(cloneLocation);
|
|
uri.setDomain(clone['URI-REPLACEMENT'].HOST);
|
|
locationsPathwayIdMap.push({
|
|
pathwayId: pathwayId,
|
|
location: uri.toString(),
|
|
});
|
|
} else {
|
|
locationsPathwayIdMap.push({
|
|
pathwayId: pathwayId,
|
|
location: cloneLocation,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const now = Date.now();
|
|
for (const uri of this.bannedLocations_.keys()) {
|
|
const bannedUntil = this.bannedLocations_.get(uri);
|
|
if (now > bannedUntil) {
|
|
this.bannedLocations_.delete(uri);
|
|
}
|
|
}
|
|
locationsPathwayIdMap = locationsPathwayIdMap.filter((l) => {
|
|
for (const uri of this.bannedLocations_.keys()) {
|
|
if (uri.includes(new goog.Uri(l.location).getDomain())) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (locationsPathwayIdMap.length) {
|
|
this.lastPathwayUsed_ = locationsPathwayIdMap[0].pathwayId;
|
|
}
|
|
|
|
const locations = locationsPathwayIdMap.map((l) => l.location);
|
|
|
|
if (!locations.length && this.defaultPathwayId_) {
|
|
for (const pathwayId of this.defaultPathwayId_.split(',')) {
|
|
const location = streamLocations.get(pathwayId);
|
|
if (location) {
|
|
this.lastPathwayUsed_ = this.defaultPathwayId_;
|
|
locations.push(location);
|
|
}
|
|
}
|
|
}
|
|
if (!locations.length) {
|
|
for (const location of streamLocations.values()) {
|
|
locations.push(location);
|
|
}
|
|
}
|
|
if (ignoreBaseUrls) {
|
|
return locations;
|
|
}
|
|
return shaka.util.ManifestParserUtils.resolveUris(
|
|
this.baseUris_, locations);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* VERSION: number,
|
|
* TTL: number,
|
|
* RELOAD-URI: string,
|
|
* PATHWAY-PRIORITY: !Array<string>,
|
|
* PATHWAY-CLONES: !Array<shaka.util.ContentSteeringManager.PathwayClone>,
|
|
* }}
|
|
*
|
|
* @description
|
|
* Contains information about the Steering Manifest
|
|
*
|
|
* @property {string} VERSION
|
|
* @property {number} TTL
|
|
* @property {string} RELOAD-URI
|
|
* @property {!Array<string>} PATHWAY-PRIORITY
|
|
* @property {!Array<
|
|
* shaka.util.ContentSteeringManager.PathwayClone>} PATHWAY-CLONES
|
|
*/
|
|
shaka.util.ContentSteeringManager.SteeringManifest;
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* BASE-ID: string,
|
|
* ID: string,
|
|
* URI-REPLACEMENT: !Array<shaka.util.ContentSteeringManager.UriReplacement>,
|
|
* }}
|
|
*
|
|
* @property {string} BASE-ID
|
|
* @property {string} ID
|
|
* @property {!Array<
|
|
* shaka.util.ContentSteeringManager.UriReplacement>} URI-REPLACEMENT
|
|
*/
|
|
shaka.util.ContentSteeringManager.PathwayClone;
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* HOST: string,
|
|
* }}
|
|
*
|
|
* @property {string} HOST
|
|
*/
|
|
shaka.util.ContentSteeringManager.UriReplacement;
|