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