Source: lib/util/cmsd_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.CmsdManager');
  7. goog.require('shaka.log');
  8. /**
  9. * @summary
  10. * A CmsdManager maintains CMSD state as well as a collection of utility
  11. * functions.
  12. * @export
  13. */
  14. shaka.util.CmsdManager = class {
  15. /**
  16. * @param {shaka.extern.CmsdConfiguration} config
  17. */
  18. constructor(config) {
  19. /** @private {shaka.extern.CmsdConfiguration} */
  20. this.config_ = config;
  21. /** @private {?Map<string, (boolean | number | string)>} */
  22. this.staticParams_ = null;
  23. /** @private {?Map<string, (boolean | number | string)>} */
  24. this.dynamicParams_ = null;
  25. }
  26. /**
  27. * Called by the Player to provide an updated configuration any time it
  28. * changes.
  29. *
  30. * @param {shaka.extern.CmsdConfiguration} config
  31. */
  32. configure(config) {
  33. this.config_ = config;
  34. }
  35. /**
  36. * Resets the CmsdManager.
  37. */
  38. reset() {
  39. this.staticParams_ = null;
  40. this.dynamicParams_ = null;
  41. }
  42. /**
  43. * Called by the Player to provide the headers of the latest request.
  44. *
  45. * @param {!Object<string, string>} headers
  46. */
  47. processHeaders(headers) {
  48. if (!this.config_.enabled) {
  49. return;
  50. }
  51. const CmsdManager = shaka.util.CmsdManager;
  52. const cmsdStatic = headers[CmsdManager.CMSD_STATIC_HEADER_NAME_];
  53. if (cmsdStatic) {
  54. const staticParams = this.parseCMSDStatic_(cmsdStatic);
  55. if (staticParams) {
  56. this.staticParams_ = staticParams;
  57. }
  58. }
  59. const cmsdDynamic = headers[CmsdManager.CMSD_DYNAMIC_HEADER_NAME_];
  60. if (cmsdDynamic) {
  61. const dynamicParams = this.parseCMSDDynamic_(cmsdDynamic);
  62. if (dynamicParams) {
  63. this.dynamicParams_ = dynamicParams;
  64. }
  65. }
  66. }
  67. /**
  68. * Returns the max bitrate in bits per second. If there is no max bitrate or
  69. * it's not enabled, it returns null.
  70. *
  71. * @return {?number}
  72. * @export
  73. */
  74. getMaxBitrate() {
  75. const key = shaka.util.CmsdManager.KEYS_.MAX_SUGGESTED_BITRATE;
  76. if (!this.config_.enabled || !this.config_.applyMaximumSuggestedBitrate ||
  77. !this.dynamicParams_ || !this.dynamicParams_.has(key)) {
  78. return null;
  79. }
  80. return /** @type {number} */(this.dynamicParams_.get(key)) * 1000;
  81. }
  82. /**
  83. * Returns the estimated throughput in bits per second. If there is no
  84. * estimated throughput or it's not enabled, it returns null.
  85. *
  86. * @return {?number}
  87. * @export
  88. */
  89. getEstimatedThroughput() {
  90. const key = shaka.util.CmsdManager.KEYS_.ESTIMATED_THROUGHPUT;
  91. if (!this.config_.enabled || !this.dynamicParams_ ||
  92. !this.dynamicParams_.has(key)) {
  93. return null;
  94. }
  95. return /** @type {number} */(this.dynamicParams_.get(key)) * 1000;
  96. }
  97. /**
  98. * Returns the response delay in milliseconds. If there is no response delay
  99. * or it's not enabled, it returns null.
  100. *
  101. * @return {?number}
  102. * @export
  103. */
  104. getResponseDelay() {
  105. const key = shaka.util.CmsdManager.KEYS_.RESPONSE_DELAY;
  106. if (!this.config_.enabled || !this.dynamicParams_ ||
  107. !this.dynamicParams_.has(key)) {
  108. return null;
  109. }
  110. return /** @type {number} */(this.dynamicParams_.get(key));
  111. }
  112. /**
  113. * Returns the RTT in milliseconds. If there is no RTT or it's not enabled,
  114. * it returns null.
  115. *
  116. * @return {?number}
  117. * @export
  118. */
  119. getRoundTripTime() {
  120. const key = shaka.util.CmsdManager.KEYS_.ROUND_TRIP_TIME;
  121. if (!this.config_.enabled || !this.dynamicParams_ ||
  122. !this.dynamicParams_.has(key)) {
  123. return null;
  124. }
  125. return /** @type {number} */(this.dynamicParams_.get(key));
  126. }
  127. /**
  128. * Gets the current bandwidth estimate.
  129. *
  130. * @param {number} defaultEstimate
  131. * @return {number} The bandwidth estimate in bits per second.
  132. * @export
  133. */
  134. getBandwidthEstimate(defaultEstimate) {
  135. const estimatedThroughput = this.getEstimatedThroughput();
  136. if (!estimatedThroughput) {
  137. return defaultEstimate;
  138. }
  139. const etpWeightRatio = this.config_.estimatedThroughputWeightRatio;
  140. if (etpWeightRatio > 0 && etpWeightRatio <= 1) {
  141. return (defaultEstimate * (1 - etpWeightRatio)) +
  142. (estimatedThroughput * etpWeightRatio);
  143. }
  144. return defaultEstimate;
  145. }
  146. /**
  147. * @param {string} headerValue
  148. * @return {?Map<string, (boolean | number | string)>}
  149. * @private
  150. */
  151. parseCMSDStatic_(headerValue) {
  152. try {
  153. const params = new Map();
  154. const items = headerValue.split(',');
  155. for (let i = 0; i < items.length; i++) {
  156. // <key>=<value>
  157. const substrings = items[i].split('=');
  158. const key = substrings[0];
  159. const value = this.parseParameterValue_(substrings[1]);
  160. params.set(key, value);
  161. }
  162. return params;
  163. } catch (e) {
  164. shaka.log.warning(
  165. 'Failed to parse CMSD-Static response header value:', e);
  166. return null;
  167. }
  168. }
  169. /**
  170. * @param {string} headerValue
  171. * @return {?Map<string, (boolean | number | string)>}
  172. * @private
  173. */
  174. parseCMSDDynamic_(headerValue) {
  175. try {
  176. const params = new Map();
  177. const items = headerValue.split(';');
  178. // Server identifier as 1st item
  179. for (let i = 1; i < items.length; i++) {
  180. // <key>=<value>
  181. const substrings = items[i].split('=');
  182. const key = substrings[0];
  183. const value = this.parseParameterValue_(substrings[1]);
  184. params.set(key, value);
  185. }
  186. return params;
  187. } catch (e) {
  188. shaka.log.warning(
  189. 'Failed to parse CMSD-Dynamic response header value:', e);
  190. return null;
  191. }
  192. }
  193. /**
  194. * @param {string} value
  195. * @return {(boolean|number|string)}
  196. * @private
  197. */
  198. parseParameterValue_(value) {
  199. // If the value type is BOOLEAN and the value is TRUE, then the equals
  200. // sign and the value are omitted
  201. if (!value) {
  202. return true;
  203. }
  204. // Check if boolean 'false'
  205. if (value.toLowerCase() === 'false') {
  206. return false;
  207. }
  208. // Check if a number
  209. if (/^[-0-9]/.test(value)) {
  210. return parseInt(value, 10);
  211. }
  212. // Value is a string, remove double quotes from string value
  213. return value.replace(/["]+/g, '');
  214. }
  215. };
  216. /**
  217. * @const {string}
  218. * @private
  219. */
  220. shaka.util.CmsdManager.CMSD_STATIC_HEADER_NAME_ = 'cmsd-static';
  221. /**
  222. * @const {string}
  223. * @private
  224. */
  225. shaka.util.CmsdManager.CMSD_DYNAMIC_HEADER_NAME_ = 'cmsd-dynamic';
  226. /**
  227. * @enum {string}
  228. * @private
  229. */
  230. shaka.util.CmsdManager.KEYS_ = {
  231. AVAILABILITY_TIME: 'at',
  232. DURESS: 'du',
  233. ENCODED_BITRATE: 'br',
  234. ESTIMATED_THROUGHPUT: 'etp',
  235. HELD_TIME: 'ht',
  236. INTERMEDIARY_IDENTIFIER: 'n',
  237. MAX_SUGGESTED_BITRATE: 'mb',
  238. NEXT_OBJECT_RESPONSE: 'nor',
  239. NEXT_RANGE_RESPONSE: 'nrr',
  240. OBJECT_DURATION: 'd',
  241. OBJECT_TYPE: 'ot',
  242. RESPONSE_DELAY: 'rd',
  243. ROUND_TRIP_TIME: 'rtt',
  244. STARTUP: 'su',
  245. STREAM_TYPE: 'st',
  246. STREAMING_FORMAT: 'sf',
  247. VERSION: 'v',
  248. };