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 {shaka.extern.Resolution} 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 {shaka.extern.Resolution} */
  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. if (!this.config_.streaming.dontChooseCodecs) {
  47. shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
  48. manifest,
  49. this.config_.preferredVideoCodecs,
  50. this.config_.preferredAudioCodecs,
  51. this.config_.preferredDecodingAttributes);
  52. }
  53. this.checkPlayableVariants_(manifest);
  54. return this.filterManifestWithRestrictions(manifest);
  55. }
  56. /**
  57. * @param {?shaka.extern.Manifest} manifest
  58. * @return {boolean} tracksChanged
  59. */
  60. applyRestrictions(manifest) {
  61. return shaka.util.StreamUtils.applyRestrictions(
  62. manifest.variants, this.config_.restrictions, this.maxHwRes_);
  63. }
  64. /**
  65. * Apply the restrictions configuration to the manifest, and check if there's
  66. * a variant that meets the restrictions.
  67. *
  68. * @param {?shaka.extern.Manifest} manifest
  69. * @return {boolean} tracksChanged
  70. */
  71. filterManifestWithRestrictions(manifest) {
  72. const tracksChanged = this.applyRestrictions(manifest);
  73. if (manifest) {
  74. // We may need to create new sessions for any new init data.
  75. const currentDrmInfo =
  76. this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
  77. // DrmEngine.newInitData() requires mediaKeys to be available.
  78. if (currentDrmInfo && this.drmEngine_.getMediaKeys()) {
  79. for (const variant of manifest.variants) {
  80. this.processDrmInfos(currentDrmInfo.keySystem, variant.video);
  81. this.processDrmInfos(currentDrmInfo.keySystem, variant.audio);
  82. }
  83. }
  84. this.checkRestrictedVariants(manifest);
  85. }
  86. return tracksChanged;
  87. }
  88. /**
  89. * Confirm some variants are playable. Otherwise, throw an exception.
  90. * @param {!shaka.extern.Manifest} manifest
  91. * @private
  92. */
  93. checkPlayableVariants_(manifest) {
  94. const valid = manifest.variants.some(shaka.util.StreamUtils.isPlayable);
  95. // If none of the variants are playable, throw
  96. // CONTENT_UNSUPPORTED_BY_BROWSER.
  97. if (!valid) {
  98. throw new shaka.util.Error(
  99. shaka.util.Error.Severity.CRITICAL,
  100. shaka.util.Error.Category.MANIFEST,
  101. shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER);
  102. }
  103. }
  104. /**
  105. * @param {string} keySystem
  106. * @param {?shaka.extern.Stream} stream
  107. */
  108. processDrmInfos(keySystem, stream) {
  109. if (!stream) {
  110. return;
  111. }
  112. for (const drmInfo of stream.drmInfos) {
  113. // Ignore any data for different key systems.
  114. if (drmInfo.keySystem == keySystem) {
  115. for (const initData of (drmInfo.initData || [])) {
  116. this.drmEngine_.newInitData(
  117. initData.initDataType, initData.initData);
  118. }
  119. }
  120. }
  121. }
  122. /**
  123. * Checks if the variants are all restricted, and throw an appropriate
  124. * exception if so.
  125. *
  126. * @param {shaka.extern.Manifest} manifest
  127. */
  128. checkRestrictedVariants(manifest) {
  129. const restrictedStatuses = shaka.media.ManifestFilterer.restrictedStatuses;
  130. const keyStatusMap =
  131. this.drmEngine_ ? this.drmEngine_.getKeyStatuses() : {};
  132. const keyIds = Object.keys(keyStatusMap);
  133. const isGlobalStatus = keyIds.length && keyIds[0] == '00';
  134. let hasPlayable = false;
  135. let hasAppRestrictions = false;
  136. /** @type {!Set.<string>} */
  137. const missingKeys = new Set();
  138. /** @type {!Set.<string>} */
  139. const badKeyStatuses = new Set();
  140. for (const variant of manifest.variants) {
  141. // TODO: Combine with onKeyStatus_.
  142. const streams = [];
  143. if (variant.audio) {
  144. streams.push(variant.audio);
  145. }
  146. if (variant.video) {
  147. streams.push(variant.video);
  148. }
  149. for (const stream of streams) {
  150. if (stream.keyIds.size) {
  151. for (const keyId of stream.keyIds) {
  152. const keyStatus = keyStatusMap[isGlobalStatus ? '00' : keyId];
  153. if (!keyStatus) {
  154. missingKeys.add(keyId);
  155. } else if (restrictedStatuses.includes(keyStatus)) {
  156. badKeyStatuses.add(keyStatus);
  157. }
  158. }
  159. } // if (stream.keyIds.size)
  160. }
  161. if (!variant.allowedByApplication) {
  162. hasAppRestrictions = true;
  163. } else if (variant.allowedByKeySystem) {
  164. hasPlayable = true;
  165. }
  166. }
  167. if (!hasPlayable) {
  168. /** @type {shaka.extern.RestrictionInfo} */
  169. const data = {
  170. hasAppRestrictions,
  171. missingKeys: Array.from(missingKeys),
  172. restrictedKeyStatuses: Array.from(badKeyStatuses),
  173. };
  174. throw new shaka.util.Error(
  175. shaka.util.Error.Severity.CRITICAL,
  176. shaka.util.Error.Category.MANIFEST,
  177. shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET,
  178. data);
  179. }
  180. }
  181. };
  182. /**
  183. * These are the EME key statuses that represent restricted playback.
  184. * 'usable', 'released', 'output-downscaled', 'status-pending' are statuses
  185. * of the usable keys. 'expired' status is being handled separately in
  186. * DrmEngine.
  187. *
  188. * @const {!Array.<string>}
  189. */
  190. shaka.media.ManifestFilterer.restrictedStatuses =
  191. ['output-restricted', 'internal-error'];