Source: lib/media/manifest_filterer.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2023 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.ManifestFilterer');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.util.StreamUtils');
  9. goog.require('shaka.media.DrmEngine');
  10. goog.require('shaka.util.Error');
  11. /**
  12. * A class that handles the filtering of manifests.
  13. * Allows for manifest filtering to be done both by the player and by a
  14. * preload manager.
  15. */
  16. shaka.media.ManifestFilterer = class {
  17. /**
  18. * @param {?shaka.extern.PlayerConfiguration} config
  19. * @param {{width: number, height: number}} maxHwRes
  20. * @param {?shaka.media.DrmEngine} drmEngine
  21. */
  22. constructor(config, maxHwRes, drmEngine) {
  23. goog.asserts.assert(config, 'Must have config');
  24. /** @private {!shaka.extern.PlayerConfiguration} */
  25. this.config_ = config;
  26. /** @private {{width: number, height: number}} */
  27. this.maxHwRes_ = maxHwRes;
  28. /** @private {?shaka.media.DrmEngine} drmEngine */
  29. this.drmEngine_ = drmEngine;
  30. }
  31. /** @param {!shaka.media.DrmEngine} drmEngine */
  32. setDrmEngine(drmEngine) {
  33. this.drmEngine_ = drmEngine;
  34. }
  35. /**
  36. * Filters a manifest, removing unplayable streams/variants and choosing
  37. * the codecs.
  38. *
  39. * @param {?shaka.extern.Manifest} manifest
  40. * @return {!Promise.<boolean>} tracksChanged
  41. */
  42. async filterManifest(manifest) {
  43. goog.asserts.assert(manifest, 'Manifest should exist!');
  44. await shaka.util.StreamUtils.filterManifest(this.drmEngine_, manifest,
  45. this.config_.drm.preferredKeySystems);
  46. shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
  47. manifest,
  48. this.config_.preferredVideoCodecs,
  49. this.config_.preferredAudioCodecs,
  50. this.config_.preferredDecodingAttributes);
  51. this.checkPlayableVariants_(manifest);
  52. return this.filterManifestWithRestrictions(manifest);
  53. }
  54. /**
  55. * @param {?shaka.extern.Manifest} manifest
  56. * @return {boolean} tracksChanged
  57. */
  58. applyRestrictions(manifest) {
  59. return shaka.util.StreamUtils.applyRestrictions(
  60. manifest.variants, this.config_.restrictions, this.maxHwRes_);
  61. }
  62. /**
  63. * Apply the restrictions configuration to the manifest, and check if there's
  64. * a variant that meets the restrictions.
  65. *
  66. * @param {?shaka.extern.Manifest} manifest
  67. * @return {boolean} tracksChanged
  68. */
  69. filterManifestWithRestrictions(manifest) {
  70. const tracksChanged = this.applyRestrictions(manifest);
  71. if (manifest) {
  72. // We may need to create new sessions for any new init data.
  73. const currentDrmInfo =
  74. this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
  75. // DrmEngine.newInitData() requires mediaKeys to be available.
  76. if (currentDrmInfo && this.drmEngine_.getMediaKeys()) {
  77. for (const variant of manifest.variants) {
  78. this.processDrmInfos(currentDrmInfo.keySystem, variant.video);
  79. this.processDrmInfos(currentDrmInfo.keySystem, variant.audio);
  80. }
  81. }
  82. this.checkRestrictedVariants(manifest);
  83. }
  84. return tracksChanged;
  85. }
  86. /**
  87. * Confirm some variants are playable. Otherwise, throw an exception.
  88. * @param {!shaka.extern.Manifest} manifest
  89. * @private
  90. */
  91. checkPlayableVariants_(manifest) {
  92. const valid = manifest.variants.some(shaka.util.StreamUtils.isPlayable);
  93. // If none of the variants are playable, throw
  94. // CONTENT_UNSUPPORTED_BY_BROWSER.
  95. if (!valid) {
  96. throw new shaka.util.Error(
  97. shaka.util.Error.Severity.CRITICAL,
  98. shaka.util.Error.Category.MANIFEST,
  99. shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER);
  100. }
  101. }
  102. /**
  103. * @param {string} keySystem
  104. * @param {?shaka.extern.Stream} stream
  105. */
  106. processDrmInfos(keySystem, stream) {
  107. if (!stream) {
  108. return;
  109. }
  110. for (const drmInfo of stream.drmInfos) {
  111. // Ignore any data for different key systems.
  112. if (drmInfo.keySystem == keySystem) {
  113. for (const initData of (drmInfo.initData || [])) {
  114. this.drmEngine_.newInitData(
  115. initData.initDataType, initData.initData);
  116. }
  117. }
  118. }
  119. }
  120. /**
  121. * Checks if the variants are all restricted, and throw an appropriate
  122. * exception if so.
  123. *
  124. * @param {shaka.extern.Manifest} manifest
  125. */
  126. checkRestrictedVariants(manifest) {
  127. const restrictedStatuses = shaka.media.ManifestFilterer.restrictedStatuses;
  128. const keyStatusMap =
  129. this.drmEngine_ ? this.drmEngine_.getKeyStatuses() : {};
  130. const keyIds = Object.keys(keyStatusMap);
  131. const isGlobalStatus = keyIds.length && keyIds[0] == '00';
  132. let hasPlayable = false;
  133. let hasAppRestrictions = false;
  134. /** @type {!Set.<string>} */
  135. const missingKeys = new Set();
  136. /** @type {!Set.<string>} */
  137. const badKeyStatuses = new Set();
  138. for (const variant of manifest.variants) {
  139. // TODO: Combine with onKeyStatus_.
  140. const streams = [];
  141. if (variant.audio) {
  142. streams.push(variant.audio);
  143. }
  144. if (variant.video) {
  145. streams.push(variant.video);
  146. }
  147. for (const stream of streams) {
  148. if (stream.keyIds.size) {
  149. for (const keyId of stream.keyIds) {
  150. const keyStatus = keyStatusMap[isGlobalStatus ? '00' : keyId];
  151. if (!keyStatus) {
  152. missingKeys.add(keyId);
  153. } else if (restrictedStatuses.includes(keyStatus)) {
  154. badKeyStatuses.add(keyStatus);
  155. }
  156. }
  157. } // if (stream.keyIds.size)
  158. }
  159. if (!variant.allowedByApplication) {
  160. hasAppRestrictions = true;
  161. } else if (variant.allowedByKeySystem) {
  162. hasPlayable = true;
  163. }
  164. }
  165. if (!hasPlayable) {
  166. /** @type {shaka.extern.RestrictionInfo} */
  167. const data = {
  168. hasAppRestrictions,
  169. missingKeys: Array.from(missingKeys),
  170. restrictedKeyStatuses: Array.from(badKeyStatuses),
  171. };
  172. throw new shaka.util.Error(
  173. shaka.util.Error.Severity.CRITICAL,
  174. shaka.util.Error.Category.MANIFEST,
  175. shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET,
  176. data);
  177. }
  178. }
  179. };
  180. /**
  181. * These are the EME key statuses that represent restricted playback.
  182. * 'usable', 'released', 'output-downscaled', 'status-pending' are statuses
  183. * of the usable keys. 'expired' status is being handled separately in
  184. * DrmEngine.
  185. *
  186. * @const {!Array.<string>}
  187. */
  188. shaka.media.ManifestFilterer.restrictedStatuses =
  189. ['output-restricted', 'internal-error'];