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, codecSwitchingStrategy, enableAudioGroups) {
  42. // We can't know if role and label are really important, so we don't use
  43. // role and label for this.
  44. const role = '';
  45. const audioLabel = '';
  46. const videoLabel = '';
  47. const hdrLevel = example.video && example.video.hdr ?
  48. example.video.hdr : '';
  49. const spatialAudio = example.audio && example.audio.spatialAudio ?
  50. example.audio.spatialAudio : false;
  51. const videoLayout = example.video && example.video.videoLayout ?
  52. example.video.videoLayout : '';
  53. const channelCount = example.audio && example.audio.channelsCount ?
  54. example.audio.channelsCount : 0;
  55. const audioCodec = example.audio && example.audio.codecs ?
  56. example.audio.codecs : '';
  57. /** @private {!shaka.media.AdaptationSetCriteria} */
  58. this.preferenceBasedCriteria_ = new shaka.media.PreferenceBasedCriteria(
  59. example.language, role, channelCount, hdrLevel, spatialAudio,
  60. videoLayout, audioLabel, videoLabel,
  61. codecSwitchingStrategy, enableAudioGroups, audioCodec);
  62. }
  63. /** @override */
  64. create(variants) {
  65. return this.preferenceBasedCriteria_.create(variants);
  66. }
  67. };
  68. /**
  69. * @implements {shaka.media.AdaptationSetCriteria}
  70. * @final
  71. */
  72. shaka.media.PreferenceBasedCriteria = class {
  73. /**
  74. * @param {string} language
  75. * @param {string} role
  76. * @param {number} channelCount
  77. * @param {string} hdrLevel
  78. * @param {boolean} spatialAudio
  79. * @param {string} videoLayout
  80. * @param {string} audioLabel
  81. * @param {string} videoLabel
  82. * @param {shaka.config.CodecSwitchingStrategy} codecSwitchingStrategy
  83. * @param {boolean} enableAudioGroups
  84. * @param {string} audioCodec
  85. */
  86. constructor(language, role, channelCount, hdrLevel, spatialAudio,
  87. videoLayout, audioLabel, videoLabel, codecSwitchingStrategy,
  88. enableAudioGroups, audioCodec) {
  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.hdrLevel_ = hdrLevel;
  97. /** @private {boolean} */
  98. this.spatialAudio_ = spatialAudio;
  99. /** @private {string} */
  100. this.videoLayout_ = videoLayout;
  101. /** @private {string} */
  102. this.audioLabel_ = audioLabel;
  103. /** @private {string} */
  104. this.videoLabel_ = videoLabel;
  105. /** @private {shaka.config.CodecSwitchingStrategy} */
  106. this.codecSwitchingStrategy_ = codecSwitchingStrategy;
  107. /** @private {boolean} */
  108. this.enableAudioGroups_ = enableAudioGroups;
  109. /** @private {string} */
  110. this.audioCodec_ = audioCodec;
  111. }
  112. /** @override */
  113. create(variants) {
  114. const Class = shaka.media.PreferenceBasedCriteria;
  115. let current = [];
  116. const byLanguage = Class.filterByLanguage_(variants, this.language_);
  117. const byPrimary = variants.filter((variant) => variant.primary);
  118. if (byLanguage.length) {
  119. current = byLanguage;
  120. } else if (byPrimary.length) {
  121. current = byPrimary;
  122. } else {
  123. current = variants;
  124. }
  125. // Now refine the choice based on role preference. Even the empty string
  126. // works here, and will match variants without any roles.
  127. const byRole = Class.filterVariantsByRole_(current, this.role_);
  128. if (byRole.length) {
  129. current = byRole;
  130. } else {
  131. shaka.log.warning('No exact match for variant role could be found.');
  132. }
  133. if (this.videoLayout_) {
  134. const byVideoLayout = Class.filterVariantsByVideoLayout_(
  135. current, this.videoLayout_);
  136. if (byVideoLayout.length) {
  137. current = byVideoLayout;
  138. } else {
  139. shaka.log.warning(
  140. 'No exact match for the video layout could be found.');
  141. }
  142. }
  143. if (this.hdrLevel_) {
  144. const byHdrLevel = Class.filterVariantsByHDRLevel_(
  145. current, this.hdrLevel_);
  146. if (byHdrLevel.length) {
  147. current = byHdrLevel;
  148. } else {
  149. shaka.log.warning(
  150. 'No exact match for the hdr level could be found.');
  151. }
  152. }
  153. if (this.channelCount_) {
  154. const byChannel = Class.filterVariantsByAudioChannelCount_(
  155. current, this.channelCount_);
  156. if (byChannel.length) {
  157. current = byChannel;
  158. } else {
  159. shaka.log.warning(
  160. 'No exact match for the channel count could be found.');
  161. }
  162. }
  163. if (this.audioLabel_) {
  164. const byLabel = Class.filterVariantsByAudioLabel_(
  165. current, this.audioLabel_);
  166. if (byLabel.length) {
  167. current = byLabel;
  168. } else {
  169. shaka.log.warning('No exact match for audio label could be found.');
  170. }
  171. }
  172. if (this.videoLabel_) {
  173. const byLabel = Class.filterVariantsByVideoLabel_(
  174. current, this.videoLabel_);
  175. if (byLabel.length) {
  176. current = byLabel;
  177. } else {
  178. shaka.log.warning('No exact match for video label could be found.');
  179. }
  180. }
  181. const bySpatialAudio = Class.filterVariantsBySpatialAudio_(
  182. current, this.spatialAudio_);
  183. if (bySpatialAudio.length) {
  184. current = bySpatialAudio;
  185. } else {
  186. shaka.log.warning('No exact match for spatial audio could be found.');
  187. }
  188. if (this.audioCodec_) {
  189. const byAudioCodec = Class.filterVariantsByAudioCodec_(
  190. current, this.audioCodec_);
  191. if (byAudioCodec.length) {
  192. current = byAudioCodec;
  193. } else {
  194. shaka.log.warning('No exact match for audio codec could be found.');
  195. }
  196. }
  197. const supportsSmoothCodecTransitions = this.codecSwitchingStrategy_ ==
  198. shaka.config.CodecSwitchingStrategy.SMOOTH &&
  199. shaka.media.Capabilities.isChangeTypeSupported();
  200. return new shaka.media.AdaptationSet(current[0], current,
  201. !supportsSmoothCodecTransitions, this.enableAudioGroups_);
  202. }
  203. /**
  204. * @param {!Array.<shaka.extern.Variant>} variants
  205. * @param {string} preferredLanguage
  206. * @return {!Array.<shaka.extern.Variant>}
  207. * @private
  208. */
  209. static filterByLanguage_(variants, preferredLanguage) {
  210. const LanguageUtils = shaka.util.LanguageUtils;
  211. /** @type {string} */
  212. const preferredLocale = LanguageUtils.normalize(preferredLanguage);
  213. /** @type {?string} */
  214. const closestLocale = LanguageUtils.findClosestLocale(
  215. preferredLocale,
  216. variants.map((variant) => LanguageUtils.getLocaleForVariant(variant)));
  217. // There were no locales close to what we preferred.
  218. if (!closestLocale) {
  219. return [];
  220. }
  221. // Find the variants that use the closest variant.
  222. return variants.filter((variant) => {
  223. return closestLocale == LanguageUtils.getLocaleForVariant(variant);
  224. });
  225. }
  226. /**
  227. * Filter Variants by role.
  228. *
  229. * @param {!Array.<shaka.extern.Variant>} variants
  230. * @param {string} preferredRole
  231. * @return {!Array.<shaka.extern.Variant>}
  232. * @private
  233. */
  234. static filterVariantsByRole_(variants, preferredRole) {
  235. return variants.filter((variant) => {
  236. if (!variant.audio) {
  237. return false;
  238. }
  239. if (preferredRole) {
  240. return variant.audio.roles.includes(preferredRole);
  241. } else {
  242. return variant.audio.roles.length == 0;
  243. }
  244. });
  245. }
  246. /**
  247. * Filter Variants by audio label.
  248. *
  249. * @param {!Array.<shaka.extern.Variant>} variants
  250. * @param {string} preferredLabel
  251. * @return {!Array.<shaka.extern.Variant>}
  252. * @private
  253. */
  254. static filterVariantsByAudioLabel_(variants, preferredLabel) {
  255. return variants.filter((variant) => {
  256. if (!variant.audio || !variant.audio.label) {
  257. return false;
  258. }
  259. const label1 = variant.audio.label.toLowerCase();
  260. const label2 = preferredLabel.toLowerCase();
  261. return label1 == label2;
  262. });
  263. }
  264. /**
  265. * Filter Variants by video label.
  266. *
  267. * @param {!Array.<shaka.extern.Variant>} variants
  268. * @param {string} preferredLabel
  269. * @return {!Array.<shaka.extern.Variant>}
  270. * @private
  271. */
  272. static filterVariantsByVideoLabel_(variants, preferredLabel) {
  273. return variants.filter((variant) => {
  274. if (!variant.video || !variant.video.label) {
  275. return false;
  276. }
  277. const label1 = variant.video.label.toLowerCase();
  278. const label2 = preferredLabel.toLowerCase();
  279. return label1 == label2;
  280. });
  281. }
  282. /**
  283. * Filter Variants by channelCount.
  284. *
  285. * @param {!Array.<shaka.extern.Variant>} variants
  286. * @param {number} channelCount
  287. * @return {!Array.<shaka.extern.Variant>}
  288. * @private
  289. */
  290. static filterVariantsByAudioChannelCount_(variants, channelCount) {
  291. return variants.filter((variant) => {
  292. if (variant.audio && variant.audio.channelsCount &&
  293. variant.audio.channelsCount != channelCount) {
  294. return false;
  295. }
  296. return true;
  297. });
  298. }
  299. /**
  300. * Filters variants according to the given hdr level config.
  301. *
  302. * @param {!Array.<shaka.extern.Variant>} variants
  303. * @param {string} hdrLevel
  304. * @private
  305. */
  306. static filterVariantsByHDRLevel_(variants, hdrLevel) {
  307. if (hdrLevel == 'AUTO') {
  308. // Auto detect the ideal HDR level.
  309. if (window.matchMedia('(color-gamut: p3)').matches) {
  310. const someHLG = variants.some((variant) => {
  311. if (variant.video && variant.video.hdr &&
  312. variant.video.hdr == 'HLG') {
  313. return true;
  314. }
  315. return false;
  316. });
  317. hdrLevel = someHLG ? 'HLG' : 'PQ';
  318. } else {
  319. hdrLevel = 'SDR';
  320. }
  321. }
  322. return variants.filter((variant) => {
  323. if (variant.video && variant.video.hdr && variant.video.hdr != hdrLevel) {
  324. return false;
  325. }
  326. return true;
  327. });
  328. }
  329. /**
  330. * Filters variants according to the given video layout config.
  331. *
  332. * @param {!Array.<shaka.extern.Variant>} variants
  333. * @param {string} videoLayout
  334. * @private
  335. */
  336. static filterVariantsByVideoLayout_(variants, videoLayout) {
  337. return variants.filter((variant) => {
  338. if (variant.video && variant.video.videoLayout &&
  339. variant.video.videoLayout != videoLayout) {
  340. return false;
  341. }
  342. return true;
  343. });
  344. }
  345. /**
  346. * Filters variants according to the given spatial audio config.
  347. *
  348. * @param {!Array.<shaka.extern.Variant>} variants
  349. * @param {boolean} spatialAudio
  350. * @private
  351. */
  352. static filterVariantsBySpatialAudio_(variants, spatialAudio) {
  353. return variants.filter((variant) => {
  354. if (variant.audio && variant.audio.spatialAudio != spatialAudio) {
  355. return false;
  356. }
  357. return true;
  358. });
  359. }
  360. /**
  361. * Filters variants according to the given audio codec.
  362. *
  363. * @param {!Array<shaka.extern.Variant>} variants
  364. * @param {string} audioCodec
  365. * @private
  366. */
  367. static filterVariantsByAudioCodec_(variants, audioCodec) {
  368. return variants.filter((variant) => {
  369. if (variant.audio && variant.audio.codecs != audioCodec) {
  370. return false;
  371. }
  372. return true;
  373. });
  374. }
  375. };