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