Source: lib/util/content_steering_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.ContentSteeringManager');
  7. goog.require('goog.Uri');
  8. goog.require('shaka.media.ManifestParser');
  9. goog.require('shaka.net.NetworkingEngine');
  10. goog.require('shaka.util.Error');
  11. goog.require('shaka.util.IDestroyable');
  12. goog.require('shaka.util.ManifestParserUtils');
  13. goog.require('shaka.util.OperationManager');
  14. goog.require('shaka.util.StringUtils');
  15. goog.require('shaka.util.Timer');
  16. /**
  17. * Create a Content Steering manager.
  18. *
  19. * @implements {shaka.util.IDestroyable}
  20. */
  21. shaka.util.ContentSteeringManager = class {
  22. /**
  23. * @param {shaka.extern.ManifestParser.PlayerInterface} playerInterface
  24. */
  25. constructor(playerInterface) {
  26. /** @private {?shaka.extern.ManifestConfiguration} */
  27. this.config_ = null;
  28. /** @private {?shaka.extern.ManifestParser.PlayerInterface} */
  29. this.playerInterface_ = playerInterface;
  30. /** @private {!shaka.util.OperationManager} */
  31. this.operationManager_ = new shaka.util.OperationManager();
  32. /** @private {!Array<string>} */
  33. this.baseUris_ = [];
  34. /** @private {?string} */
  35. this.defaultPathwayId_ = null;
  36. /** @private {!Array<string>} */
  37. this.pathwayPriority_ = [];
  38. /** @private {?string} */
  39. this.lastPathwayUsed_ = null;
  40. /** @private {!Array<shaka.util.ContentSteeringManager.PathwayClone>} */
  41. this.pathwayClones_ = [];
  42. /**
  43. * Default to 5 minutes. Value in seconds.
  44. *
  45. * @private {number}
  46. */
  47. this.lastTTL_ = 300;
  48. /** @private {!Map<(string | number), !Map<string, string>>} */
  49. this.locations_ = new Map();
  50. /** @private {!Map<string, number>} */
  51. this.bannedLocations_ = new Map();
  52. /** @private {?shaka.util.Timer} */
  53. this.updateTimer_ = null;
  54. /** @private {string} */
  55. this.manifestType_ = shaka.media.ManifestParser.UNKNOWN;
  56. }
  57. /**
  58. * @param {shaka.extern.ManifestConfiguration} config
  59. */
  60. configure(config) {
  61. this.config_ = config;
  62. }
  63. /** @override */
  64. destroy() {
  65. this.config_ = null;
  66. this.playerInterface_ = null;
  67. this.baseUris_ = [];
  68. this.defaultPathwayId_ = null;
  69. this.pathwayPriority_ = [];
  70. this.pathwayClones_ = [];
  71. this.locations_.clear();
  72. if (this.updateTimer_ != null) {
  73. this.updateTimer_.stop();
  74. this.updateTimer_ = null;
  75. }
  76. return this.operationManager_.destroy();
  77. }
  78. /**
  79. * @param {string} manifestType
  80. */
  81. setManifestType(manifestType) {
  82. this.manifestType_ = manifestType;
  83. }
  84. /**
  85. * @param {!Array<string>} baseUris
  86. */
  87. setBaseUris(baseUris) {
  88. this.baseUris_ = baseUris;
  89. }
  90. /**
  91. * @param {?string} defaultPathwayId
  92. */
  93. setDefaultPathwayId(defaultPathwayId) {
  94. this.defaultPathwayId_ = defaultPathwayId;
  95. }
  96. /**
  97. * Request the Content Steering info.
  98. *
  99. * @param {string} uri
  100. * @return {!Promise}
  101. */
  102. async requestInfo(uri) {
  103. const uris = shaka.util.ManifestParserUtils.resolveUris(
  104. this.baseUris_, [this.addQueryParams_(uri)]);
  105. const type = shaka.net.NetworkingEngine.RequestType.CONTENT_STEERING;
  106. const request = shaka.net.NetworkingEngine.makeRequest(
  107. uris, this.config_.retryParameters);
  108. const op = this.playerInterface_.networkingEngine.request(type, request);
  109. this.operationManager_.manage(op);
  110. try {
  111. const response = await op.promise;
  112. const str = shaka.util.StringUtils.fromUTF8(response.data);
  113. const steeringManifest =
  114. /** @type {shaka.util.ContentSteeringManager.SteeringManifest} */
  115. (JSON.parse(str));
  116. if (steeringManifest.VERSION == 1) {
  117. this.processManifest_(steeringManifest, response.uri);
  118. }
  119. } catch (e) {
  120. if (e && e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  121. return;
  122. }
  123. if (this.updateTimer_ != null) {
  124. this.updateTimer_.stop();
  125. this.updateTimer_ = null;
  126. }
  127. this.updateTimer_ = new shaka.util.Timer(() => {
  128. this.requestInfo(uri);
  129. });
  130. this.updateTimer_.tickAfter(this.lastTTL_);
  131. }
  132. }
  133. /** @private */
  134. addQueryParams_(uri) {
  135. if (!this.pathwayPriority_.length) {
  136. return uri;
  137. }
  138. const finalUri = new goog.Uri(uri);
  139. const currentPathwayID = this.lastPathwayUsed_ || this.pathwayPriority_[0];
  140. const currentBandwidth =
  141. Math.round(this.playerInterface_.getBandwidthEstimate());
  142. const queryData = finalUri.getQueryData();
  143. if (this.manifestType_ == shaka.media.ManifestParser.DASH) {
  144. queryData.add('_DASH_pathway', currentPathwayID);
  145. queryData.add('_DASH_throughput', String(currentBandwidth));
  146. } else if (this.manifestType_ == shaka.media.ManifestParser.HLS) {
  147. queryData.add('_HLS_pathway', currentPathwayID);
  148. queryData.add('_HLS_throughput', String(currentBandwidth));
  149. }
  150. if (queryData.getCount()) {
  151. finalUri.setQueryData(queryData);
  152. }
  153. return finalUri.toString();
  154. }
  155. /**
  156. * @param {shaka.util.ContentSteeringManager.SteeringManifest} manifest
  157. * @param {string} finalManifestUri
  158. * @private
  159. */
  160. processManifest_(manifest, finalManifestUri) {
  161. if (this.updateTimer_ != null) {
  162. this.updateTimer_.stop();
  163. this.updateTimer_ = null;
  164. }
  165. const uri = manifest['RELOAD-URI'] || finalManifestUri;
  166. this.updateTimer_ = new shaka.util.Timer(() => {
  167. this.requestInfo(uri);
  168. });
  169. const newTTL = manifest['TTL'];
  170. if (newTTL) {
  171. this.lastTTL_ = newTTL;
  172. }
  173. this.updateTimer_.tickAfter(this.lastTTL_);
  174. this.pathwayPriority_ = manifest['PATHWAY-PRIORITY'] || [];
  175. this.pathwayClones_ = manifest['PATHWAY-CLONES'] || [];
  176. }
  177. /**
  178. * Clear the previous locations added.
  179. */
  180. clearPreviousLocations() {
  181. this.locations_.clear();
  182. }
  183. /**
  184. * @param {string|number} streamId
  185. * @param {string} pathwayId
  186. * @param {string} uri
  187. */
  188. addLocation(streamId, pathwayId, uri) {
  189. let streamLocations = this.locations_.get(streamId);
  190. if (!streamLocations) {
  191. streamLocations = new Map();
  192. }
  193. streamLocations.set(pathwayId, uri);
  194. this.locations_.set(streamId, streamLocations);
  195. }
  196. /**
  197. * @param {string} uri
  198. */
  199. banLocation(uri) {
  200. const bannedUntil = Date.now() + 60000;
  201. this.bannedLocations_.set(uri, bannedUntil);
  202. }
  203. /**
  204. * Get the base locations ordered according the priority.
  205. *
  206. * @param {string|number} streamId
  207. * @param {boolean=} ignoreBaseUrls
  208. * @return {!Array<string>}
  209. */
  210. getLocations(streamId, ignoreBaseUrls = false) {
  211. const streamLocations = this.locations_.get(streamId) || new Map();
  212. /** @type {!Array<!{pathwayId: string, location: string}>} */
  213. let locationsPathwayIdMap = [];
  214. for (const pathwayId of this.pathwayPriority_) {
  215. const location = streamLocations.get(pathwayId);
  216. if (location) {
  217. locationsPathwayIdMap.push({pathwayId, location});
  218. } else {
  219. const clone = this.pathwayClones_.find((c) => c.ID == pathwayId);
  220. if (clone) {
  221. const cloneLocation = streamLocations.get(clone['BASE-ID']);
  222. if (cloneLocation) {
  223. if (clone['URI-REPLACEMENT'].HOST) {
  224. const uri = new goog.Uri(cloneLocation);
  225. uri.setDomain(clone['URI-REPLACEMENT'].HOST);
  226. locationsPathwayIdMap.push({
  227. pathwayId: pathwayId,
  228. location: uri.toString(),
  229. });
  230. } else {
  231. locationsPathwayIdMap.push({
  232. pathwayId: pathwayId,
  233. location: cloneLocation,
  234. });
  235. }
  236. }
  237. }
  238. }
  239. }
  240. const now = Date.now();
  241. for (const uri of this.bannedLocations_.keys()) {
  242. const bannedUntil = this.bannedLocations_.get(uri);
  243. if (now > bannedUntil) {
  244. this.bannedLocations_.delete(uri);
  245. }
  246. }
  247. locationsPathwayIdMap = locationsPathwayIdMap.filter((l) => {
  248. for (const uri of this.bannedLocations_.keys()) {
  249. if (uri.includes(new goog.Uri(l.location).getDomain())) {
  250. return false;
  251. }
  252. }
  253. return true;
  254. });
  255. if (locationsPathwayIdMap.length) {
  256. this.lastPathwayUsed_ = locationsPathwayIdMap[0].pathwayId;
  257. }
  258. const locations = locationsPathwayIdMap.map((l) => l.location);
  259. if (!locations.length && this.defaultPathwayId_) {
  260. for (const pathwayId of this.defaultPathwayId_.split(',')) {
  261. const location = streamLocations.get(pathwayId);
  262. if (location) {
  263. this.lastPathwayUsed_ = this.defaultPathwayId_;
  264. locations.push(location);
  265. }
  266. }
  267. }
  268. if (!locations.length) {
  269. for (const location of streamLocations.values()) {
  270. locations.push(location);
  271. }
  272. }
  273. if (ignoreBaseUrls) {
  274. return locations;
  275. }
  276. return shaka.util.ManifestParserUtils.resolveUris(
  277. this.baseUris_, locations);
  278. }
  279. };
  280. /**
  281. * @typedef {{
  282. * VERSION: number,
  283. * TTL: number,
  284. * RELOAD-URI: string,
  285. * PATHWAY-PRIORITY: !Array<string>,
  286. * PATHWAY-CLONES: !Array<shaka.util.ContentSteeringManager.PathwayClone>
  287. * }}
  288. *
  289. * @description
  290. * Contains information about the Steering Manifest
  291. *
  292. * @property {string} VERSION
  293. * @property {number} TTL
  294. * @property {string} RELOAD-URI
  295. * @property {!Array<string>} PATHWAY-PRIORITY
  296. * @property {!Array<
  297. * shaka.util.ContentSteeringManager.PathwayClone>} PATHWAY-CLONES
  298. */
  299. shaka.util.ContentSteeringManager.SteeringManifest;
  300. /**
  301. * @typedef {{
  302. * BASE-ID: string,
  303. * ID: string,
  304. * URI-REPLACEMENT: !Array<shaka.util.ContentSteeringManager.UriReplacement>
  305. * }}
  306. *
  307. * @property {string} BASE-ID
  308. * @property {string} ID
  309. * @property {!Array<
  310. * shaka.util.ContentSteeringManager.UriReplacement>} URI-REPLACEMENT
  311. */
  312. shaka.util.ContentSteeringManager.PathwayClone;
  313. /**
  314. * @typedef {{
  315. * HOST: string
  316. * }}
  317. *
  318. * @property {string} HOST
  319. */
  320. shaka.util.ContentSteeringManager.UriReplacement;