Source: lib/media/adaptation_set_criteria.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.AdaptationSetCriteria');
  7. goog.provide('shaka.media.ExampleBasedCriteria');
  8. goog.provide('shaka.media.PreferenceBasedCriteria');
  9. goog.require('shaka.log');
  10. goog.require('shaka.media.AdaptationSet');
  11. goog.require('shaka.util.LanguageUtils');
  12. goog.require('shaka.util.StreamUtils');
  13. /**
  14. * An adaptation set criteria is a unit of logic that can take a set of
  15. * variants and return a subset of variants that should (and can) be
  16. * adapted between.
  17. *
  18. * @interface
  19. */
  20. shaka.media.AdaptationSetCriteria = class {
  21. /**
  22. * Take a set of variants, and return a subset of variants that can be
  23. * adapted between.
  24. *
  25. * @param {!Array.<shaka.extern.Variant>} variants
  26. * @return {!shaka.media.AdaptationSet}
  27. */
  28. create(variants) {}
  29. };
  30. /**
  31. * @implements {shaka.media.AdaptationSetCriteria}
  32. * @final
  33. */
  34. shaka.media.ExampleBasedCriteria = class {
  35. /**
  36. * @param {shaka.extern.Variant} example
  37. */
  38. constructor(example) {
  39. /** @private {shaka.extern.Variant} */
  40. this.example_ = example;
  41. // We can't know if role and label are really important, so we don't use
  42. // role and label for this.
  43. const role = '';
  44. const label = '';
  45. const channelCount = example.audio && example.audio.channelsCount ?
  46. example.audio.channelsCount :
  47. 0;
  48. /** @private {!shaka.media.AdaptationSetCriteria} */
  49. this.fallback_ = new shaka.media.PreferenceBasedCriteria(
  50. example.language, role, channelCount, label);
  51. }
  52. /** @override */
  53. create(variants) {
  54. // We can't assume that the example is in |variants| because it could
  55. // actually be from another period.
  56. const shortList = variants.filter((variant) => {
  57. return shaka.media.AdaptationSet.areAdaptable(this.example_, variant);
  58. });
  59. if (shortList.length) {
  60. // Use the first item in the short list as the root. It should not matter
  61. // which element we use as all items in the short list should already be
  62. // compatible.
  63. return new shaka.media.AdaptationSet(shortList[0], shortList);
  64. } else {
  65. return this.fallback_.create(variants);
  66. }
  67. }
  68. };
  69. /**
  70. * @implements {shaka.media.AdaptationSetCriteria}
  71. * @final
  72. */
  73. shaka.media.PreferenceBasedCriteria = class {
  74. /**
  75. * @param {string} language
  76. * @param {string} role
  77. * @param {number} channelCount
  78. * @param {string=} label
  79. */
  80. constructor(language, role, channelCount, label = '') {
  81. /** @private {string} */
  82. this.language_ = language;
  83. /** @private {string} */
  84. this.role_ = role;
  85. /** @private {number} */
  86. this.channelCount_ = channelCount;
  87. /** @private {string} */
  88. this.label_ = label;
  89. }
  90. /** @override */
  91. create(variants) {
  92. const Class = shaka.media.PreferenceBasedCriteria;
  93. const StreamUtils = shaka.util.StreamUtils;
  94. let current = [];
  95. const byLanguage = Class.filterByLanguage_(variants, this.language_);
  96. const byPrimary = variants.filter((variant) => variant.primary);
  97. if (byLanguage.length) {
  98. current = byLanguage;
  99. } else if (byPrimary.length) {
  100. current = byPrimary;
  101. } else {
  102. current = variants;
  103. }
  104. // Now refine the choice based on role preference. Even the empty string
  105. // works here, and will match variants without any roles.
  106. const byRole = Class.filterVariantsByRole_(current, this.role_);
  107. if (byRole.length) {
  108. current = byRole;
  109. } else {
  110. shaka.log.warning('No exact match for variant role could be found.');
  111. }
  112. if (this.channelCount_) {
  113. const byChannel = StreamUtils.filterVariantsByAudioChannelCount(
  114. current, this.channelCount_);
  115. if (byChannel.length) {
  116. current = byChannel;
  117. } else {
  118. shaka.log.warning(
  119. 'No exact match for the channel count could be found.');
  120. }
  121. }
  122. if (this.label_) {
  123. const byLabel = Class.filterVariantsByLabel_(current, this.label_);
  124. if (byLabel.length) {
  125. current = byLabel;
  126. } else {
  127. shaka.log.warning('No exact match for variant label could be found.');
  128. }
  129. }
  130. // Make sure we only return a valid adaptation set.
  131. const set = new shaka.media.AdaptationSet(current[0]);
  132. for (const variant of current) {
  133. if (set.canInclude(variant)) {
  134. set.add(variant);
  135. }
  136. }
  137. return set;
  138. }
  139. /**
  140. * @param {!Array.<shaka.extern.Variant>} variants
  141. * @param {string} preferredLanguage
  142. * @return {!Array.<shaka.extern.Variant>}
  143. * @private
  144. */
  145. static filterByLanguage_(variants, preferredLanguage) {
  146. const LanguageUtils = shaka.util.LanguageUtils;
  147. /** @type {string} */
  148. const preferredLocale = LanguageUtils.normalize(preferredLanguage);
  149. /** @type {?string} */
  150. const closestLocale = LanguageUtils.findClosestLocale(
  151. preferredLocale,
  152. variants.map((variant) => LanguageUtils.getLocaleForVariant(variant)));
  153. // There were no locales close to what we preferred.
  154. if (!closestLocale) {
  155. return [];
  156. }
  157. // Find the variants that use the closest variant.
  158. return variants.filter((variant) => {
  159. return closestLocale == LanguageUtils.getLocaleForVariant(variant);
  160. });
  161. }
  162. /**
  163. * Filter Variants by role.
  164. *
  165. * @param {!Array.<shaka.extern.Variant>} variants
  166. * @param {string} preferredRole
  167. * @return {!Array.<shaka.extern.Variant>}
  168. * @private
  169. */
  170. static filterVariantsByRole_(variants, preferredRole) {
  171. return variants.filter((variant) => {
  172. if (!variant.audio) {
  173. return false;
  174. }
  175. if (preferredRole) {
  176. return variant.audio.roles.includes(preferredRole);
  177. } else {
  178. return variant.audio.roles.length == 0;
  179. }
  180. });
  181. }
  182. /**
  183. * Filter Variants by label.
  184. *
  185. * @param {!Array.<shaka.extern.Variant>} variants
  186. * @param {string} preferredLabel
  187. * @return {!Array.<shaka.extern.Variant>}
  188. * @private
  189. */
  190. static filterVariantsByLabel_(variants, preferredLabel) {
  191. return variants.filter((variant) => {
  192. if (!variant.audio) {
  193. return false;
  194. }
  195. const label1 = variant.audio.label.toLowerCase();
  196. const label2 = preferredLabel.toLowerCase();
  197. return label1 == label2;
  198. });
  199. }
  200. };