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