Source: lib/media/adaptation_set.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.AdaptationSet');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.util.MimeUtils');
  10. /**
  11. * A set of variants that we want to adapt between.
  12. *
  13. * @final
  14. */
  15. shaka.media.AdaptationSet = class {
  16. /**
  17. * @param {shaka.extern.Variant} root
  18. * The variant that all other variants will be tested against when being
  19. * added to the adaptation set. If a variant is not compatible with the
  20. * root, it will not be added.
  21. * @param {!Iterable.<shaka.extern.Variant>=} candidates
  22. * Variants that may be compatible with the root and should be added if
  23. * compatible. If a candidate is not compatible, it will not end up in the
  24. * adaptation set.
  25. * @param {boolean=} compareCodecs
  26. * @param {boolean=} enableAudioGroups
  27. */
  28. constructor(root, candidates, compareCodecs = true,
  29. enableAudioGroups = false) {
  30. /** @private {shaka.extern.Variant} */
  31. this.root_ = root;
  32. /** @private {!Set.<shaka.extern.Variant>} */
  33. this.variants_ = new Set([root]);
  34. // Try to add all the candidates. If they cannot be added (because they
  35. // are not compatible with the root, they will be rejected by |add|.
  36. candidates = candidates || [];
  37. for (const candidate of candidates) {
  38. this.add(candidate, compareCodecs, enableAudioGroups);
  39. }
  40. }
  41. /**
  42. * @param {shaka.extern.Variant} variant
  43. * @param {boolean} compareCodecs
  44. * @param {boolean} enableAudioGroups
  45. * @return {boolean}
  46. */
  47. add(variant, compareCodecs, enableAudioGroups) {
  48. if (this.canInclude(variant, compareCodecs, enableAudioGroups)) {
  49. this.variants_.add(variant);
  50. return true;
  51. }
  52. // To be nice, issue a warning if someone is trying to add something that
  53. // they shouldn't.
  54. shaka.log.warning('Rejecting variant - not compatible with root.');
  55. return false;
  56. }
  57. /**
  58. * Check if |variant| can be included with the set. If |canInclude| returns
  59. * |false|, calling |add| will result in it being ignored.
  60. *
  61. * @param {shaka.extern.Variant} variant
  62. * @param {boolean=} compareCodecs
  63. * @param {boolean=} enableAudioGroups
  64. * @return {boolean}
  65. */
  66. canInclude(variant, compareCodecs = true, enableAudioGroups = false) {
  67. return shaka.media.AdaptationSet
  68. .areAdaptable(this.root_, variant, compareCodecs, enableAudioGroups);
  69. }
  70. /**
  71. * @param {shaka.extern.Variant} a
  72. * @param {shaka.extern.Variant} b
  73. * @param {boolean} compareCodecs
  74. * @param {boolean} enableAudioGroups
  75. * @return {boolean}
  76. */
  77. static areAdaptable(a, b, compareCodecs, enableAudioGroups) {
  78. const AdaptationSet = shaka.media.AdaptationSet;
  79. // All variants should have audio or should all not have audio.
  80. if (!!a.audio != !!b.audio) {
  81. return false;
  82. }
  83. // All variants should have video or should all not have video.
  84. if (!!a.video != !!b.video) {
  85. return false;
  86. }
  87. // If the languages don't match, we should not adapt between them.
  88. if (a.language != b.language) {
  89. return false;
  90. }
  91. goog.asserts.assert(
  92. !!a.audio == !!b.audio,
  93. 'Both should either have audio or not have audio.');
  94. if (a.audio && b.audio && !AdaptationSet.areAudiosCompatible_(
  95. a.audio, b.audio, compareCodecs, enableAudioGroups)) {
  96. return false;
  97. }
  98. goog.asserts.assert(
  99. !!a.video == !!b.video,
  100. 'Both should either have video or not have video.');
  101. if (a.video && b.video &&
  102. !AdaptationSet.areVideosCompatible_(a.video, b.video, compareCodecs)) {
  103. return false;
  104. }
  105. return true;
  106. }
  107. /**
  108. * @return {!Iterable.<shaka.extern.Variant>}
  109. */
  110. values() {
  111. return this.variants_.values();
  112. }
  113. /**
  114. * Check if we can switch between two audio streams.
  115. *
  116. * @param {shaka.extern.Stream} a
  117. * @param {shaka.extern.Stream} b
  118. * @param {boolean} compareCodecs
  119. * @param {boolean} enableAudioGroups
  120. * @return {boolean}
  121. * @private
  122. */
  123. static areAudiosCompatible_(a, b, compareCodecs, enableAudioGroups) {
  124. const AdaptationSet = shaka.media.AdaptationSet;
  125. // Don't adapt between channel counts, which could annoy the user
  126. // due to volume changes on downmixing. An exception is made for
  127. // stereo and mono, which should be fine to adapt between.
  128. if (!a.channelsCount || !b.channelsCount ||
  129. a.channelsCount > 2 || b.channelsCount > 2) {
  130. if (a.channelsCount != b.channelsCount) {
  131. return false;
  132. }
  133. }
  134. // Don't adapt between spatial and non spatial audio, which may
  135. // annoy the user.
  136. if (a.spatialAudio !== b.spatialAudio) {
  137. return false;
  138. }
  139. // We can only adapt between base-codecs.
  140. if (compareCodecs && !AdaptationSet.canTransitionBetween_(a, b)) {
  141. return false;
  142. }
  143. // Audio roles must not change between adaptations.
  144. if (!AdaptationSet.areRolesEqual_(a.roles, b.roles)) {
  145. return false;
  146. }
  147. // We can only adapt between the same groupId.
  148. if (enableAudioGroups && a.groupId !== b.groupId) {
  149. return false;
  150. }
  151. return true;
  152. }
  153. /**
  154. * Check if we can switch between two video streams.
  155. *
  156. * @param {shaka.extern.Stream} a
  157. * @param {shaka.extern.Stream} b
  158. * @param {boolean} compareCodecs
  159. * @return {boolean}
  160. * @private
  161. */
  162. static areVideosCompatible_(a, b, compareCodecs) {
  163. const AdaptationSet = shaka.media.AdaptationSet;
  164. // We can only adapt between base-codecs.
  165. if (compareCodecs && !AdaptationSet.canTransitionBetween_(a, b)) {
  166. return false;
  167. }
  168. // Video roles must not change between adaptations.
  169. if (!AdaptationSet.areRolesEqual_(a.roles, b.roles)) {
  170. return false;
  171. }
  172. return true;
  173. }
  174. /**
  175. * Check if we can switch between two streams based on their codec and mime
  176. * type.
  177. *
  178. * @param {shaka.extern.Stream} a
  179. * @param {shaka.extern.Stream} b
  180. * @return {boolean}
  181. * @private
  182. */
  183. static canTransitionBetween_(a, b) {
  184. if (a.mimeType != b.mimeType) {
  185. return false;
  186. }
  187. // Get the base codec of each codec in each stream.
  188. const codecsA = shaka.util.MimeUtils.splitCodecs(a.codecs).map((codec) => {
  189. return shaka.util.MimeUtils.getCodecBase(codec);
  190. });
  191. const codecsB = shaka.util.MimeUtils.splitCodecs(b.codecs).map((codec) => {
  192. return shaka.util.MimeUtils.getCodecBase(codec);
  193. });
  194. // We don't want to allow switching between transmuxed and non-transmuxed
  195. // content so the number of codecs should be the same.
  196. //
  197. // To avoid the case where an codec is used for audio and video we will
  198. // codecs using arrays (not sets). While at this time, there are no codecs
  199. // that work for audio and video, it is possible for "raw" codecs to be
  200. // which would share the same name.
  201. if (codecsA.length != codecsB.length) {
  202. return false;
  203. }
  204. // Sort them so that we can walk through them and compare them
  205. // element-by-element.
  206. codecsA.sort();
  207. codecsB.sort();
  208. for (let i = 0; i < codecsA.length; i++) {
  209. if (codecsA[i] != codecsB[i]) {
  210. return false;
  211. }
  212. }
  213. return true;
  214. }
  215. /**
  216. * Check if two role lists are the equal. This will take into account all
  217. * unique behaviours when comparing roles.
  218. *
  219. * @param {!Iterable.<string>} a
  220. * @param {!Iterable.<string>} b
  221. * @return {boolean}
  222. * @private
  223. */
  224. static areRolesEqual_(a, b) {
  225. const aSet = new Set(a);
  226. const bSet = new Set(b);
  227. // Remove the main role from the role lists (we expect to see them only
  228. // in dash manifests).
  229. const mainRole = 'main';
  230. aSet.delete(mainRole);
  231. bSet.delete(mainRole);
  232. // Make sure that we have the same number roles in each list. Make sure to
  233. // do it after correcting for 'main'.
  234. if (aSet.size != bSet.size) {
  235. return false;
  236. }
  237. // Because we know the two sets are the same size, if any item is missing
  238. // if means that they are not the same.
  239. for (const x of aSet) {
  240. if (!bSet.has(x)) {
  241. return false;
  242. }
  243. }
  244. return true;
  245. }
  246. };