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.config.CodecSwitchingStrategy');
  10. goog.require('shaka.log');
  11. goog.require('shaka.media.AdaptationSet');
  12. goog.require('shaka.media.Capabilities');
  13. goog.require('shaka.util.LanguageUtils');
  14. /**
  15. * An adaptation set criteria is a unit of logic that can take a set of
  16. * variants and return a subset of variants that should (and can) be
  17. * adapted between.
  18. *
  19. * @interface
  20. */
  21. shaka.media.AdaptationSetCriteria = class {
  22. /**
  23. * Take a set of variants, and return a subset of variants that can be
  24. * adapted between.
  25. *
  26. * @param {!Array.<shaka.extern.Variant>} variants
  27. * @return {!shaka.media.AdaptationSet}
  28. */
  29. create(variants) {}
  30. };
  31. /**
  32. * @implements {shaka.media.AdaptationSetCriteria}
  33. * @final
  34. */
  35. shaka.media.ExampleBasedCriteria = class {
  36. /**
  37. * @param {shaka.extern.Variant} example
  38. * @param {shaka.config.CodecSwitchingStrategy=} codecSwitchingStrategy
  39. * @param {boolean=} enableAudioGroups
  40. */
  41. constructor(example,
  42. codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD,
  43. enableAudioGroups = false) {
  44. /** @private {shaka.extern.Variant} */
  45. this.example_ = example;
  46. /** @private {shaka.config.CodecSwitchingStrategy} */
  47. this.codecSwitchingStrategy_ = codecSwitchingStrategy;
  48. /** @private {boolean} */
  49. this.enableAudioGroups_ = enableAudioGroups;
  50. // We can't know if role and label are really important, so we don't use
  51. // role and label for this.
  52. const role = '';
  53. const label = '';
  54. const hdrLevel = '';
  55. const videoLayout = '';
  56. const channelCount = example.audio && example.audio.channelsCount ?
  57. example.audio.channelsCount :
  58. 0;
  59. /** @private {!shaka.media.AdaptationSetCriteria} */
  60. this.fallback_ = new shaka.media.PreferenceBasedCriteria(
  61. example.language, role, channelCount, hdrLevel, videoLayout, label,
  62. codecSwitchingStrategy, enableAudioGroups);
  63. }
  64. /** @override */
  65. create(variants) {
  66. const supportsSmoothCodecTransitions = this.codecSwitchingStrategy_ ==
  67. shaka.config.CodecSwitchingStrategy.SMOOTH &&
  68. shaka.media.Capabilities.isChangeTypeSupported();
  69. // We can't assume that the example is in |variants| because it could
  70. // actually be from another period.
  71. const shortList = variants.filter((variant) => {
  72. return shaka.media.AdaptationSet.areAdaptable(this.example_, variant,
  73. !supportsSmoothCodecTransitions, this.enableAudioGroups_);
  74. });
  75. if (shortList.length) {
  76. // Use the first item in the short list as the root. It should not matter
  77. // which element we use as all items in the short list should already be
  78. // compatible.
  79. return new shaka.media.AdaptationSet(shortList[0], shortList,
  80. !supportsSmoothCodecTransitions, this.enableAudioGroups_);
  81. } else {
  82. return this.fallback_.create(variants);
  83. }
  84. }
  85. };
  86. /**
  87. * @implements {shaka.media.AdaptationSetCriteria}
  88. * @final
  89. */
  90. shaka.media.PreferenceBasedCriteria = class {
  91. /**
  92. * @param {string} language
  93. * @param {string} role
  94. * @param {number} channelCount
  95. * @param {string} hdrLevel
  96. * @param {string} videoLayout
  97. * @param {string=} label
  98. * @param {shaka.config.CodecSwitchingStrategy=} codecSwitchingStrategy
  99. * @param {boolean=} enableAudioGroups
  100. */
  101. constructor(language, role, channelCount, hdrLevel, videoLayout, label = '',
  102. codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD,
  103. enableAudioGroups = false) {
  104. /** @private {string} */
  105. this.language_ = language;
  106. /** @private {string} */
  107. this.role_ = role;
  108. /** @private {number} */
  109. this.channelCount_ = channelCount;
  110. /** @private {string} */
  111. this.hdrLevel_ = hdrLevel;
  112. /** @private {string} */
  113. this.videoLayout_ = videoLayout;
  114. /** @private {string} */
  115. this.label_ = label;
  116. /** @private {shaka.config.CodecSwitchingStrategy} */
  117. this.codecSwitchingStrategy_ = codecSwitchingStrategy;
  118. /** @private {boolean} */
  119. this.enableAudioGroups_ = enableAudioGroups;
  120. }
  121. /** @override */
  122. create(variants) {
  123. const Class = shaka.media.PreferenceBasedCriteria;
  124. let current = [];
  125. const byLanguage = Class.filterByLanguage_(variants, this.language_);
  126. const byPrimary = variants.filter((variant) => variant.primary);
  127. if (byLanguage.length) {
  128. current = byLanguage;
  129. } else if (byPrimary.length) {
  130. current = byPrimary;
  131. } else {
  132. current = variants;
  133. }
  134. // Now refine the choice based on role preference. Even the empty string
  135. // works here, and will match variants without any roles.
  136. const byRole = Class.filterVariantsByRole_(current, this.role_);
  137. if (byRole.length) {
  138. current = byRole;
  139. } else {
  140. shaka.log.warning('No exact match for variant role could be found.');
  141. }
  142. if (this.videoLayout_) {
  143. const byVideoLayout = Class.filterVariantsByVideoLayout_(
  144. current, this.videoLayout_);
  145. if (byVideoLayout.length) {
  146. current = byVideoLayout;
  147. } else {
  148. shaka.log.warning(
  149. 'No exact match for the video layout could be found.');
  150. }
  151. }
  152. if (this.hdrLevel_) {
  153. const byHdrLevel = Class.filterVariantsByHDRLevel_(
  154. current, this.hdrLevel_);
  155. if (byHdrLevel.length) {
  156. current = byHdrLevel;
  157. } else {
  158. shaka.log.warning(
  159. 'No exact match for the hdr level could be found.');
  160. }
  161. }
  162. if (this.channelCount_) {
  163. const byChannel = Class.filterVariantsByAudioChannelCount_(
  164. current, this.channelCount_);
  165. if (byChannel.length) {
  166. current = byChannel;
  167. } else {
  168. shaka.log.warning(
  169. 'No exact match for the channel count could be found.');
  170. }
  171. }
  172. if (this.label_) {
  173. const byLabel = Class.filterVariantsByLabel_(current, this.label_);
  174. if (byLabel.length) {
  175. current = byLabel;
  176. } else {
  177. shaka.log.warning('No exact match for variant label could be found.');
  178. }
  179. }
  180. const supportsSmoothCodecTransitions = this.codecSwitchingStrategy_ ==
  181. shaka.config.CodecSwitchingStrategy.SMOOTH &&
  182. shaka.media.Capabilities.isChangeTypeSupported();
  183. return new shaka.media.AdaptationSet(current[0], current,
  184. !supportsSmoothCodecTransitions, this.enableAudioGroups_);
  185. }
  186. /**
  187. * @param {!Array.<shaka.extern.Variant>} variants
  188. * @param {string} preferredLanguage
  189. * @return {!Array.<shaka.extern.Variant>}
  190. * @private
  191. */
  192. static filterByLanguage_(variants, preferredLanguage) {
  193. const LanguageUtils = shaka.util.LanguageUtils;
  194. /** @type {string} */
  195. const preferredLocale = LanguageUtils.normalize(preferredLanguage);
  196. /** @type {?string} */
  197. const closestLocale = LanguageUtils.findClosestLocale(
  198. preferredLocale,
  199. variants.map((variant) => LanguageUtils.getLocaleForVariant(variant)));
  200. // There were no locales close to what we preferred.
  201. if (!closestLocale) {
  202. return [];
  203. }
  204. // Find the variants that use the closest variant.
  205. return variants.filter((variant) => {
  206. return closestLocale == LanguageUtils.getLocaleForVariant(variant);
  207. });
  208. }
  209. /**
  210. * Filter Variants by role.
  211. *
  212. * @param {!Array.<shaka.extern.Variant>} variants
  213. * @param {string} preferredRole
  214. * @return {!Array.<shaka.extern.Variant>}
  215. * @private
  216. */
  217. static filterVariantsByRole_(variants, preferredRole) {
  218. return variants.filter((variant) => {
  219. if (!variant.audio) {
  220. return false;
  221. }
  222. if (preferredRole) {
  223. return variant.audio.roles.includes(preferredRole);
  224. } else {
  225. return variant.audio.roles.length == 0;
  226. }
  227. });
  228. }
  229. /**
  230. * Filter Variants by label.
  231. *
  232. * @param {!Array.<shaka.extern.Variant>} variants
  233. * @param {string} preferredLabel
  234. * @return {!Array.<shaka.extern.Variant>}
  235. * @private
  236. */
  237. static filterVariantsByLabel_(variants, preferredLabel) {
  238. return variants.filter((variant) => {
  239. if (!variant.audio || !variant.audio.label) {
  240. return false;
  241. }
  242. const label1 = variant.audio.label.toLowerCase();
  243. const label2 = preferredLabel.toLowerCase();
  244. return label1 == label2;
  245. });
  246. }
  247. /**
  248. * Filter Variants by channelCount.
  249. *
  250. * @param {!Array.<shaka.extern.Variant>} variants
  251. * @param {number} channelCount
  252. * @return {!Array.<shaka.extern.Variant>}
  253. * @private
  254. */
  255. static filterVariantsByAudioChannelCount_(variants, channelCount) {
  256. return variants.filter((variant) => {
  257. if (variant.audio && variant.audio.channelsCount &&
  258. variant.audio.channelsCount != channelCount) {
  259. return false;
  260. }
  261. return true;
  262. });
  263. }
  264. /**
  265. * Filters variants according to the given hdr level config.
  266. *
  267. * @param {!Array.<shaka.extern.Variant>} variants
  268. * @param {string} hdrLevel
  269. * @private
  270. */
  271. static filterVariantsByHDRLevel_(variants, hdrLevel) {
  272. if (hdrLevel == 'AUTO') {
  273. // Auto detect the ideal HDR level.
  274. if (window.matchMedia('(color-gamut: p3)').matches) {
  275. hdrLevel = 'PQ';
  276. } else {
  277. hdrLevel = 'SDR';
  278. }
  279. }
  280. return variants.filter((variant) => {
  281. if (variant.video && variant.video.hdr && variant.video.hdr != hdrLevel) {
  282. return false;
  283. }
  284. return true;
  285. });
  286. }
  287. /**
  288. * Filters variants according to the given video layout config.
  289. *
  290. * @param {!Array.<shaka.extern.Variant>} variants
  291. * @param {string} videoLayout
  292. * @private
  293. */
  294. static filterVariantsByVideoLayout_(variants, videoLayout) {
  295. return variants.filter((variant) => {
  296. if (variant.video && variant.video.videoLayout &&
  297. variant.video.videoLayout != videoLayout) {
  298. return false;
  299. }
  300. return true;
  301. });
  302. }
  303. };