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