Home Reference Source

src/controller/cap-level-controller.ts

  1. /*
  2. * cap stream level to media size dimension controller
  3. */
  4.  
  5. import { Events } from '../events';
  6. import type { Level } from '../types/level';
  7. import type {
  8. ManifestParsedData,
  9. BufferCodecsData,
  10. MediaAttachingData,
  11. FPSDropLevelCappingData,
  12. } from '../types/events';
  13. import StreamController from './stream-controller';
  14. import type { ComponentAPI } from '../types/component-api';
  15. import type Hls from '../hls';
  16.  
  17. class CapLevelController implements ComponentAPI {
  18. public autoLevelCapping: number;
  19. public firstLevel: number;
  20. public media: HTMLVideoElement | null;
  21. public restrictedLevels: Array<number>;
  22. public timer: number | undefined;
  23.  
  24. private hls: Hls;
  25. private streamController?: StreamController;
  26. public clientRect: { width: number; height: number } | null;
  27.  
  28. constructor(hls: Hls) {
  29. this.hls = hls;
  30. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  31. this.firstLevel = -1;
  32. this.media = null;
  33. this.restrictedLevels = [];
  34. this.timer = undefined;
  35. this.clientRect = null;
  36.  
  37. this.registerListeners();
  38. }
  39.  
  40. public setStreamController(streamController: StreamController) {
  41. this.streamController = streamController;
  42. }
  43.  
  44. public destroy() {
  45. this.unregisterListener();
  46. if (this.hls.config.capLevelToPlayerSize) {
  47. this.stopCapping();
  48. }
  49. this.media = null;
  50. this.clientRect = null;
  51. // @ts-ignore
  52. this.hls = this.streamController = null;
  53. }
  54.  
  55. protected registerListeners() {
  56. const { hls } = this;
  57. hls.on(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
  58. hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
  59. hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  60. hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this);
  61. hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  62. }
  63.  
  64. protected unregisterListener() {
  65. const { hls } = this;
  66. hls.off(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
  67. hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
  68. hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  69. hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
  70. hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  71. }
  72.  
  73. protected onFpsDropLevelCapping(
  74. event: Events.FPS_DROP_LEVEL_CAPPING,
  75. data: FPSDropLevelCappingData
  76. ) {
  77. // Don't add a restricted level more than once
  78. if (
  79. CapLevelController.isLevelAllowed(
  80. data.droppedLevel,
  81. this.restrictedLevels
  82. )
  83. ) {
  84. this.restrictedLevels.push(data.droppedLevel);
  85. }
  86. }
  87.  
  88. protected onMediaAttaching(
  89. event: Events.MEDIA_ATTACHING,
  90. data: MediaAttachingData
  91. ) {
  92. this.media = data.media instanceof HTMLVideoElement ? data.media : null;
  93. }
  94.  
  95. protected onManifestParsed(
  96. event: Events.MANIFEST_PARSED,
  97. data: ManifestParsedData
  98. ) {
  99. const hls = this.hls;
  100. this.restrictedLevels = [];
  101. this.firstLevel = data.firstLevel;
  102. if (hls.config.capLevelToPlayerSize && data.video) {
  103. // Start capping immediately if the manifest has signaled video codecs
  104. this.startCapping();
  105. }
  106. }
  107.  
  108. // Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted
  109. // to the first level
  110. protected onBufferCodecs(
  111. event: Events.BUFFER_CODECS,
  112. data: BufferCodecsData
  113. ) {
  114. const hls = this.hls;
  115. if (hls.config.capLevelToPlayerSize && data.video) {
  116. // If the manifest did not signal a video codec capping has been deferred until we're certain video is present
  117. this.startCapping();
  118. }
  119. }
  120.  
  121. protected onMediaDetaching() {
  122. this.stopCapping();
  123. }
  124.  
  125. detectPlayerSize() {
  126. if (this.media && this.mediaHeight > 0 && this.mediaWidth > 0) {
  127. const levels = this.hls.levels;
  128. if (levels.length) {
  129. const hls = this.hls;
  130. hls.autoLevelCapping = this.getMaxLevel(levels.length - 1);
  131. if (
  132. hls.autoLevelCapping > this.autoLevelCapping &&
  133. this.streamController
  134. ) {
  135. // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch
  136. // usually happen when the user go to the fullscreen mode.
  137. this.streamController.nextLevelSwitch();
  138. }
  139. this.autoLevelCapping = hls.autoLevelCapping;
  140. }
  141. }
  142. }
  143.  
  144. /*
  145. * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
  146. */
  147. getMaxLevel(capLevelIndex: number): number {
  148. const levels = this.hls.levels;
  149. if (!levels.length) {
  150. return -1;
  151. }
  152.  
  153. const validLevels = levels.filter(
  154. (level, index) =>
  155. CapLevelController.isLevelAllowed(index, this.restrictedLevels) &&
  156. index <= capLevelIndex
  157. );
  158.  
  159. this.clientRect = null;
  160. return CapLevelController.getMaxLevelByMediaSize(
  161. validLevels,
  162. this.mediaWidth,
  163. this.mediaHeight
  164. );
  165. }
  166.  
  167. startCapping() {
  168. if (this.timer) {
  169. // Don't reset capping if started twice; this can happen if the manifest signals a video codec
  170. return;
  171. }
  172. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  173. this.hls.firstLevel = this.getMaxLevel(this.firstLevel);
  174. self.clearInterval(this.timer);
  175. this.timer = self.setInterval(this.detectPlayerSize.bind(this), 1000);
  176. this.detectPlayerSize();
  177. }
  178.  
  179. stopCapping() {
  180. this.restrictedLevels = [];
  181. this.firstLevel = -1;
  182. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  183. if (this.timer) {
  184. self.clearInterval(this.timer);
  185. this.timer = undefined;
  186. }
  187. }
  188.  
  189. getDimensions(): { width: number; height: number } {
  190. if (this.clientRect) {
  191. return this.clientRect;
  192. }
  193. const media = this.media;
  194. const boundsRect = {
  195. width: 0,
  196. height: 0,
  197. };
  198.  
  199. if (media) {
  200. const clientRect = media.getBoundingClientRect();
  201. boundsRect.width = clientRect.width;
  202. boundsRect.height = clientRect.height;
  203. if (!boundsRect.width && !boundsRect.height) {
  204. // When the media element has no width or height (equivalent to not being in the DOM),
  205. // then use its width and height attributes (media.width, media.height)
  206. boundsRect.width =
  207. clientRect.right - clientRect.left || media.width || 0;
  208. boundsRect.height =
  209. clientRect.bottom - clientRect.top || media.height || 0;
  210. }
  211. }
  212. this.clientRect = boundsRect;
  213. return boundsRect;
  214. }
  215.  
  216. get mediaWidth(): number {
  217. return this.getDimensions().width * this.contentScaleFactor;
  218. }
  219.  
  220. get mediaHeight(): number {
  221. return this.getDimensions().height * this.contentScaleFactor;
  222. }
  223.  
  224. get contentScaleFactor(): number {
  225. let pixelRatio = 1;
  226. if (!this.hls.config.ignoreDevicePixelRatio) {
  227. try {
  228. pixelRatio = self.devicePixelRatio;
  229. } catch (e) {
  230. /* no-op */
  231. }
  232. }
  233.  
  234. return pixelRatio;
  235. }
  236.  
  237. static isLevelAllowed(
  238. level: number,
  239. restrictedLevels: Array<number> = []
  240. ): boolean {
  241. return restrictedLevels.indexOf(level) === -1;
  242. }
  243.  
  244. static getMaxLevelByMediaSize(
  245. levels: Array<Level>,
  246. width: number,
  247. height: number
  248. ): number {
  249. if (!levels || !levels.length) {
  250. return -1;
  251. }
  252.  
  253. // Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next
  254. // to determine whether we've chosen the greatest bandwidth for the media's dimensions
  255. const atGreatestBandwidth = (curLevel, nextLevel) => {
  256. if (!nextLevel) {
  257. return true;
  258. }
  259.  
  260. return (
  261. curLevel.width !== nextLevel.width ||
  262. curLevel.height !== nextLevel.height
  263. );
  264. };
  265.  
  266. // If we run through the loop without breaking, the media's dimensions are greater than every level, so default to
  267. // the max level
  268. let maxLevelIndex = levels.length - 1;
  269.  
  270. for (let i = 0; i < levels.length; i += 1) {
  271. const level = levels[i];
  272. if (
  273. (level.width >= width || level.height >= height) &&
  274. atGreatestBandwidth(level, levels[i + 1])
  275. ) {
  276. maxLevelIndex = i;
  277. break;
  278. }
  279. }
  280.  
  281. return maxLevelIndex;
  282. }
  283. }
  284.  
  285. export default CapLevelController;