Source: lib/media/adaptation_set_criteria.js

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