Source: lib/util/cmcd_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.CmcdManager');
  7. goog.require('goog.Uri');
  8. goog.require('shaka.log');
  9. /**
  10. * @summary
  11. * A CmcdManager maintains CMCD state as well as a collection of utility
  12. * functions.
  13. */
  14. shaka.util.CmcdManager = class {
  15. /**
  16. * @param {shaka.util.CmcdManager.PlayerInterface} playerInterface
  17. * @param {shaka.extern.CmcdConfiguration} config
  18. */
  19. constructor(playerInterface, config) {
  20. /** @private {shaka.util.CmcdManager.PlayerInterface} */
  21. this.playerInterface_ = playerInterface;
  22. /** @private {?shaka.extern.CmcdConfiguration} */
  23. this.config_ = config;
  24. /**
  25. * Session ID
  26. *
  27. * @private {string}
  28. */
  29. this.sid_ = '';
  30. /**
  31. * Streaming format
  32. *
  33. * @private {(shaka.util.CmcdManager.StreamingFormat|undefined)}
  34. */
  35. this.sf_ = undefined;
  36. /**
  37. * @private {boolean}
  38. */
  39. this.playbackStarted_ = false;
  40. /**
  41. * @private {boolean}
  42. */
  43. this.buffering_ = true;
  44. /**
  45. * @private {boolean}
  46. */
  47. this.starved_ = false;
  48. }
  49. /**
  50. * Set the buffering state
  51. *
  52. * @param {boolean} buffering
  53. */
  54. setBuffering(buffering) {
  55. if (!buffering && !this.playbackStarted_) {
  56. this.playbackStarted_ = true;
  57. }
  58. if (this.playbackStarted_ && buffering) {
  59. this.starved_ = true;
  60. }
  61. this.buffering_ = buffering;
  62. }
  63. /**
  64. * Apply CMCD data to a manifest request.
  65. *
  66. * @param {!shaka.extern.Request} request
  67. * The request to apply CMCD data to
  68. * @param {shaka.util.CmcdManager.ManifestInfo} manifestInfo
  69. * The manifest format
  70. */
  71. applyManifestData(request, manifestInfo) {
  72. try {
  73. if (!this.config_.enabled) {
  74. return;
  75. }
  76. this.sf_ = manifestInfo.format;
  77. this.apply_(request, {
  78. ot: shaka.util.CmcdManager.ObjectType.MANIFEST,
  79. su: !this.playbackStarted_,
  80. });
  81. } catch (error) {
  82. shaka.log.warnOnce('CMCD_MANIFEST_ERROR',
  83. 'Could not generate manifest CMCD data.', error);
  84. }
  85. }
  86. /**
  87. * Apply CMCD data to a segment request
  88. *
  89. * @param {!shaka.extern.Request} request
  90. * @param {shaka.util.CmcdManager.SegmentInfo} segmentInfo
  91. */
  92. applySegmentData(request, segmentInfo) {
  93. try {
  94. if (!this.config_.enabled) {
  95. return;
  96. }
  97. const data = {
  98. d: segmentInfo.duration * 1000,
  99. st: this.getStreamType_(),
  100. };
  101. data.ot = this.getObjectType_(segmentInfo);
  102. const ObjectType = shaka.util.CmcdManager.ObjectType;
  103. const isMedia = data.ot === ObjectType.VIDEO ||
  104. data.ot === ObjectType.AUDIO ||
  105. data.ot === ObjectType.MUXED ||
  106. data.ot === ObjectType.TIMED_TEXT;
  107. if (isMedia) {
  108. data.bl = this.getBufferLength_(segmentInfo.type);
  109. }
  110. if (segmentInfo.bandwidth) {
  111. data.br = segmentInfo.bandwidth / 1000;
  112. }
  113. if (isMedia && data.ot !== ObjectType.TIMED_TEXT) {
  114. data.tb = this.getTopBandwidth_(data.ot) / 1000;
  115. }
  116. this.apply_(request, data);
  117. } catch (error) {
  118. shaka.log.warnOnce('CMCD_SEGMENT_ERROR',
  119. 'Could not generate segment CMCD data.', error);
  120. }
  121. }
  122. /**
  123. * Apply CMCD data to a text request
  124. *
  125. * @param {!shaka.extern.Request} request
  126. */
  127. applyTextData(request) {
  128. try {
  129. if (!this.config_.enabled) {
  130. return;
  131. }
  132. this.apply_(request, {
  133. ot: shaka.util.CmcdManager.ObjectType.CAPTION,
  134. su: true,
  135. });
  136. } catch (error) {
  137. shaka.log.warnOnce('CMCD_TEXT_ERROR',
  138. 'Could not generate text CMCD data.', error);
  139. }
  140. }
  141. /**
  142. * Apply CMCD data to streams loaded via src=.
  143. *
  144. * @param {string} uri
  145. * @param {string} mimeType
  146. * @return {string}
  147. */
  148. appendSrcData(uri, mimeType) {
  149. try {
  150. if (!this.config_.enabled) {
  151. return uri;
  152. }
  153. const data = this.createData_();
  154. data.ot = this.getObjectTypeFromMimeType_(mimeType);
  155. data.su = true;
  156. const query = shaka.util.CmcdManager.toQuery(data);
  157. return shaka.util.CmcdManager.appendQueryToUri(uri, query);
  158. } catch (error) {
  159. shaka.log.warnOnce('CMCD_SRC_ERROR',
  160. 'Could not generate src CMCD data.', error);
  161. return uri;
  162. }
  163. }
  164. /**
  165. * Apply CMCD data to side car text track uri.
  166. *
  167. * @param {string} uri
  168. * @return {string}
  169. */
  170. appendTextTrackData(uri) {
  171. try {
  172. if (!this.config_.enabled) {
  173. return uri;
  174. }
  175. const data = this.createData_();
  176. data.ot = shaka.util.CmcdManager.ObjectType.CAPTION;
  177. data.su = true;
  178. const query = shaka.util.CmcdManager.toQuery(data);
  179. return shaka.util.CmcdManager.appendQueryToUri(uri, query);
  180. } catch (error) {
  181. shaka.log.warnOnce('CMCD_TEXT_TRACK_ERROR',
  182. 'Could not generate text track CMCD data.', error);
  183. return uri;
  184. }
  185. }
  186. /**
  187. * Create baseline CMCD data
  188. *
  189. * @return {CmcdData}
  190. * @private
  191. */
  192. createData_() {
  193. if (!this.sid_) {
  194. this.sid_ = this.config_.sessionId || window.crypto.randomUUID();
  195. }
  196. return {
  197. v: shaka.util.CmcdManager.Version,
  198. sf: this.sf_,
  199. sid: this.sid_,
  200. cid: this.config_.contentId,
  201. mtp: this.playerInterface_.getBandwidthEstimate() / 1000,
  202. };
  203. }
  204. /**
  205. * Apply CMCD data to a request.
  206. *
  207. * @param {!shaka.extern.Request} request The request to apply CMCD data to
  208. * @param {!CmcdData} data The data object
  209. * @param {boolean} useHeaders Send data via request headers
  210. * @private
  211. */
  212. apply_(request, data = {}, useHeaders = this.config_.useHeaders) {
  213. if (!this.config_.enabled) {
  214. return;
  215. }
  216. // apply baseline data
  217. Object.assign(data, this.createData_());
  218. data.pr = this.playerInterface_.getPlaybackRate();
  219. const isVideo = data.ot === shaka.util.CmcdManager.ObjectType.VIDEO ||
  220. data.ot === shaka.util.CmcdManager.ObjectType.MUXED;
  221. if (this.starved_ && isVideo) {
  222. data.bs = true;
  223. data.su = true;
  224. this.starved_ = false;
  225. }
  226. if (data.su == null) {
  227. data.su = this.buffering_;
  228. }
  229. // TODO: Implement rtp, nrr, nor, dl
  230. if (useHeaders) {
  231. const headers = shaka.util.CmcdManager.toHeaders(data);
  232. if (!Object.keys(headers).length) {
  233. return;
  234. }
  235. Object.assign(request.headers, headers);
  236. } else {
  237. const query = shaka.util.CmcdManager.toQuery(data);
  238. if (!query) {
  239. return;
  240. }
  241. request.uris = request.uris.map((uri) => {
  242. return shaka.util.CmcdManager.appendQueryToUri(uri, query);
  243. });
  244. }
  245. }
  246. /**
  247. * The CMCD object type.
  248. *
  249. * @param {shaka.util.CmcdManager.SegmentInfo} segmentInfo
  250. * @private
  251. */
  252. getObjectType_(segmentInfo) {
  253. const type = segmentInfo.type;
  254. if (segmentInfo.init) {
  255. return shaka.util.CmcdManager.ObjectType.INIT;
  256. }
  257. if (type == 'video') {
  258. if (segmentInfo.codecs.includes(',')) {
  259. return shaka.util.CmcdManager.ObjectType.MUXED;
  260. }
  261. return shaka.util.CmcdManager.ObjectType.VIDEO;
  262. }
  263. if (type == 'audio') {
  264. return shaka.util.CmcdManager.ObjectType.AUDIO;
  265. }
  266. if (type == 'text') {
  267. if (segmentInfo.mimeType === 'application/mp4') {
  268. return shaka.util.CmcdManager.ObjectType.TIMED_TEXT;
  269. }
  270. return shaka.util.CmcdManager.ObjectType.CAPTION;
  271. }
  272. return undefined;
  273. }
  274. /**
  275. * The CMCD object type from mimeType.
  276. *
  277. * @param {!string} mimeType
  278. * @return {(shaka.util.CmcdManager.ObjectType|undefined)}
  279. * @private
  280. */
  281. getObjectTypeFromMimeType_(mimeType) {
  282. switch (mimeType) {
  283. case 'video/webm':
  284. case 'video/mp4':
  285. return shaka.util.CmcdManager.ObjectType.MUXED;
  286. case 'application/x-mpegurl':
  287. return shaka.util.CmcdManager.ObjectType.MANIFEST;
  288. default:
  289. return undefined;
  290. }
  291. }
  292. /**
  293. * Get the buffer length for a media type in milliseconds
  294. *
  295. * @param {string} type
  296. * @return {number}
  297. * @private
  298. */
  299. getBufferLength_(type) {
  300. const ranges = this.playerInterface_.getBufferedInfo()[type];
  301. if (!ranges.length) {
  302. return NaN;
  303. }
  304. const start = this.playerInterface_.getCurrentTime();
  305. const range = ranges.find((r) => r.start <= start && r.end >= start);
  306. if (!range) {
  307. return NaN;
  308. }
  309. return (range.end - start) * 1000;
  310. }
  311. /**
  312. * Get the stream type
  313. *
  314. * @return {shaka.util.CmcdManager.StreamType}
  315. * @private
  316. */
  317. getStreamType_() {
  318. const isLive = this.playerInterface_.isLive();
  319. if (isLive) {
  320. return shaka.util.CmcdManager.StreamType.LIVE;
  321. } else {
  322. return shaka.util.CmcdManager.StreamType.VOD;
  323. }
  324. }
  325. /**
  326. * Get the highest bandwidth for a given type.
  327. *
  328. * @param {string} type
  329. * @return {number}
  330. * @private
  331. */
  332. getTopBandwidth_(type) {
  333. const variants = this.playerInterface_.getVariantTracks();
  334. if (!variants.length) {
  335. return NaN;
  336. }
  337. let top = variants[0];
  338. for (const variant of variants) {
  339. if (variant.type === 'variant' && variant.bandwidth > top.bandwidth) {
  340. top = variant;
  341. }
  342. }
  343. const ObjectType = shaka.util.CmcdManager.ObjectType;
  344. switch (type) {
  345. case ObjectType.VIDEO:
  346. return top.videoBandwidth || NaN;
  347. case ObjectType.AUDIO:
  348. return top.audioBandwidth || NaN;
  349. default:
  350. return top.bandwidth;
  351. }
  352. }
  353. /**
  354. * Serialize a CMCD data object according to the rules defined in the
  355. * section 3.2 of
  356. * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf).
  357. *
  358. * @param {CmcdData} data The CMCD data object
  359. * @return {string}
  360. */
  361. static serialize(data) {
  362. const results = [];
  363. const isValid = (value) =>
  364. !Number.isNaN(value) && value != null && value !== '' && value !== false;
  365. const toRounded = (value) => Math.round(value);
  366. const toHundred = (value) => toRounded(value / 100) * 100;
  367. const toUrlSafe = (value) => encodeURIComponent(value);
  368. const formatters = {
  369. br: toRounded,
  370. d: toRounded,
  371. bl: toHundred,
  372. dl: toHundred,
  373. mtp: toHundred,
  374. nor: toUrlSafe,
  375. rtp: toHundred,
  376. tb: toRounded,
  377. };
  378. const keys = Object.keys(data || {}).sort();
  379. for (const key of keys) {
  380. let value = data[key];
  381. // ignore invalid values
  382. if (!isValid(value)) {
  383. continue;
  384. }
  385. // Version should only be reported if not equal to 1.
  386. if (key === 'v' && value === 1) {
  387. continue;
  388. }
  389. // Playback rate should only be sent if not equal to 1.
  390. if (key == 'pr' && value === 1) {
  391. continue;
  392. }
  393. // Certain values require special formatting
  394. const formatter = formatters[key];
  395. if (formatter) {
  396. value = formatter(value);
  397. }
  398. // Serialize the key/value pair
  399. const type = typeof value;
  400. let result;
  401. if (type === 'string' && key !== 'ot' && key !== 'sf' && key !== 'st') {
  402. result = `${key}=${JSON.stringify(value)}`;
  403. } else if (type === 'boolean') {
  404. result = key;
  405. } else if (type === 'symbol') {
  406. result = `${key}=${value.description}`;
  407. } else {
  408. result = `${key}=${value}`;
  409. }
  410. results.push(result);
  411. }
  412. return results.join(',');
  413. }
  414. /**
  415. * Convert a CMCD data object to request headers according to the rules
  416. * defined in the section 2.1 and 3.2 of
  417. * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf).
  418. *
  419. * @param {CmcdData} data The CMCD data object
  420. * @return {!Object}
  421. */
  422. static toHeaders(data) {
  423. const keys = Object.keys(data);
  424. const headers = {};
  425. const headerNames = ['Object', 'Request', 'Session', 'Status'];
  426. const headerGroups = [{}, {}, {}, {}];
  427. const headerMap = {
  428. br: 0, d: 0, ot: 0, tb: 0,
  429. bl: 1, dl: 1, mtp: 1, nor: 1, nrr: 1, su: 1,
  430. cid: 2, pr: 2, sf: 2, sid: 2, st: 2, v: 2,
  431. bs: 3, rtp: 3,
  432. };
  433. for (const key of keys) {
  434. // Unmapped fields are mapped to the Request header
  435. const index = (headerMap[key] != null) ? headerMap[key] : 1;
  436. headerGroups[index][key] = data[key];
  437. }
  438. for (let i = 0; i < headerGroups.length; i++) {
  439. const value = shaka.util.CmcdManager.serialize(headerGroups[i]);
  440. if (value) {
  441. headers[`CMCD-${headerNames[i]}`] = value;
  442. }
  443. }
  444. return headers;
  445. }
  446. /**
  447. * Convert a CMCD data object to query args according to the rules
  448. * defined in the section 2.2 and 3.2 of
  449. * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf).
  450. *
  451. * @param {CmcdData} data The CMCD data object
  452. * @return {string}
  453. */
  454. static toQuery(data) {
  455. return shaka.util.CmcdManager.serialize(data);
  456. }
  457. /**
  458. * Append query args to a uri.
  459. *
  460. * @param {string} uri
  461. * @param {string} query
  462. * @return {string}
  463. */
  464. static appendQueryToUri(uri, query) {
  465. if (!query) {
  466. return uri;
  467. }
  468. if (uri.includes('offline:')) {
  469. return uri;
  470. }
  471. const url = new goog.Uri(uri);
  472. url.getQueryData().set('CMCD', query);
  473. return url.toString();
  474. }
  475. };
  476. /**
  477. * @typedef {{
  478. * getBandwidthEstimate: function():number,
  479. * getBufferedInfo: function():shaka.extern.BufferedInfo,
  480. * getCurrentTime: function():number,
  481. * getVariantTracks: function():Array.<shaka.extern.Track>,
  482. * getPlaybackRate: function():number,
  483. * isLive: function():boolean
  484. * }}
  485. *
  486. * @property {function():number} getBandwidthEstimate
  487. * Get the estimated bandwidth in bits per second.
  488. * @property {function():shaka.extern.BufferedInfo} getBufferedInfo
  489. * Get information about what the player has buffered.
  490. * @property {function():number} getCurrentTime
  491. * Get the current time
  492. * @property {function():Array.<shaka.extern.Track>} getVariantTracks
  493. * Get the variant tracks
  494. * @property {function():number} getPlaybackRate
  495. * Get the playback rate
  496. * @property {function():boolean} isLive
  497. * Get if the player is playing live content.
  498. */
  499. shaka.util.CmcdManager.PlayerInterface;
  500. /**
  501. * @typedef {{
  502. * type: string,
  503. * init: boolean,
  504. * duration: number,
  505. * mimeType: string,
  506. * codecs: string,
  507. * bandwidth: (number|undefined)
  508. * }}
  509. *
  510. * @property {string} type
  511. * The media type
  512. * @property {boolean} init
  513. * Flag indicating whether the segment is an init segment
  514. * @property {number} duration
  515. * The duration of the segment in seconds
  516. * @property {string} mimeType
  517. * The segment's mime type
  518. * @property {string} codecs
  519. * The segment's codecs
  520. * @property {(number|undefined)} bandwidth
  521. * The segment's variation bandwidth
  522. *
  523. * @export
  524. */
  525. shaka.util.CmcdManager.SegmentInfo;
  526. /**
  527. * @typedef {{
  528. * format: shaka.util.CmcdManager.StreamingFormat
  529. * }}
  530. *
  531. * @property {shaka.util.CmcdManager.StreamingFormat} format
  532. * The manifest's stream format
  533. *
  534. * @export
  535. */
  536. shaka.util.CmcdManager.ManifestInfo;
  537. /**
  538. * @enum {string}
  539. */
  540. shaka.util.CmcdManager.ObjectType = {
  541. MANIFEST: 'm',
  542. AUDIO: 'a',
  543. VIDEO: 'v',
  544. MUXED: 'av',
  545. INIT: 'i',
  546. CAPTION: 'c',
  547. TIMED_TEXT: 'tt',
  548. KEY: 'k',
  549. OTHER: 'o',
  550. };
  551. /**
  552. * @enum {string}
  553. */
  554. shaka.util.CmcdManager.StreamType = {
  555. VOD: 'v',
  556. LIVE: 'l',
  557. };
  558. /**
  559. * @enum {string}
  560. * @export
  561. */
  562. shaka.util.CmcdManager.StreamingFormat = {
  563. DASH: 'd',
  564. HLS: 'h',
  565. SMOOTH: 's',
  566. OTHER: 'o',
  567. };
  568. /**
  569. * The CMCD spec version
  570. * @const {number}
  571. */
  572. shaka.util.CmcdManager.Version = 1;