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