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