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