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