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