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. this.clientRect = null;
  94. }
  95.  
  96. protected onManifestParsed(
  97. event: Events.MANIFEST_PARSED,
  98. data: ManifestParsedData
  99. ) {
  100. const hls = this.hls;
  101. this.restrictedLevels = [];
  102. this.firstLevel = data.firstLevel;
  103. if (hls.config.capLevelToPlayerSize && data.video) {
  104. // Start capping immediately if the manifest has signaled video codecs
  105. this.startCapping();
  106. }
  107. }
  108.  
  109. // Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted
  110. // to the first level
  111. protected onBufferCodecs(
  112. event: Events.BUFFER_CODECS,
  113. data: BufferCodecsData
  114. ) {
  115. const hls = this.hls;
  116. if (hls.config.capLevelToPlayerSize && data.video) {
  117. // If the manifest did not signal a video codec capping has been deferred until we're certain video is present
  118. this.startCapping();
  119. }
  120. }
  121.  
  122. protected onMediaDetaching() {
  123. this.stopCapping();
  124. }
  125.  
  126. detectPlayerSize() {
  127. if (this.media && this.mediaHeight > 0 && this.mediaWidth > 0) {
  128. const levels = this.hls.levels;
  129. if (levels.length) {
  130. const hls = this.hls;
  131. hls.autoLevelCapping = this.getMaxLevel(levels.length - 1);
  132. if (
  133. hls.autoLevelCapping > this.autoLevelCapping &&
  134. this.streamController
  135. ) {
  136. // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch
  137. // usually happen when the user go to the fullscreen mode.
  138. this.streamController.nextLevelSwitch();
  139. }
  140. this.autoLevelCapping = hls.autoLevelCapping;
  141. }
  142. }
  143. }
  144.  
  145. /*
  146. * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
  147. */
  148. getMaxLevel(capLevelIndex: number): number {
  149. const levels = this.hls.levels;
  150. if (!levels.length) {
  151. return -1;
  152. }
  153.  
  154. const validLevels = levels.filter(
  155. (level, index) =>
  156. CapLevelController.isLevelAllowed(index, this.restrictedLevels) &&
  157. index <= capLevelIndex
  158. );
  159.  
  160. this.clientRect = null;
  161. return CapLevelController.getMaxLevelByMediaSize(
  162. validLevels,
  163. this.mediaWidth,
  164. this.mediaHeight
  165. );
  166. }
  167.  
  168. startCapping() {
  169. if (this.timer) {
  170. // Don't reset capping if started twice; this can happen if the manifest signals a video codec
  171. return;
  172. }
  173. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  174. this.hls.firstLevel = this.getMaxLevel(this.firstLevel);
  175. self.clearInterval(this.timer);
  176. this.timer = self.setInterval(this.detectPlayerSize.bind(this), 1000);
  177. this.detectPlayerSize();
  178. }
  179.  
  180. stopCapping() {
  181. this.restrictedLevels = [];
  182. this.firstLevel = -1;
  183. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  184. if (this.timer) {
  185. self.clearInterval(this.timer);
  186. this.timer = undefined;
  187. }
  188. }
  189.  
  190. getDimensions(): { width: number; height: number } {
  191. if (this.clientRect) {
  192. return this.clientRect;
  193. }
  194. const media = this.media;
  195. const boundsRect = {
  196. width: 0,
  197. height: 0,
  198. };
  199.  
  200. if (media) {
  201. const clientRect = media.getBoundingClientRect();
  202. boundsRect.width = clientRect.width;
  203. boundsRect.height = clientRect.height;
  204. if (!boundsRect.width && !boundsRect.height) {
  205. // When the media element has no width or height (equivalent to not being in the DOM),
  206. // then use its width and height attributes (media.width, media.height)
  207. boundsRect.width =
  208. clientRect.right - clientRect.left || media.width || 0;
  209. boundsRect.height =
  210. clientRect.bottom - clientRect.top || media.height || 0;
  211. }
  212. }
  213. this.clientRect = boundsRect;
  214. return boundsRect;
  215. }
  216.  
  217. get mediaWidth(): number {
  218. return this.getDimensions().width * this.contentScaleFactor;
  219. }
  220.  
  221. get mediaHeight(): number {
  222. return this.getDimensions().height * this.contentScaleFactor;
  223. }
  224.  
  225. get contentScaleFactor(): number {
  226. let pixelRatio = 1;
  227. if (!this.hls.config.ignoreDevicePixelRatio) {
  228. try {
  229. pixelRatio = self.devicePixelRatio;
  230. } catch (e) {
  231. /* no-op */
  232. }
  233. }
  234.  
  235. return pixelRatio;
  236. }
  237.  
  238. static isLevelAllowed(
  239. level: number,
  240. restrictedLevels: Array<number> = []
  241. ): boolean {
  242. return restrictedLevels.indexOf(level) === -1;
  243. }
  244.  
  245. static getMaxLevelByMediaSize(
  246. levels: Array<Level>,
  247. width: number,
  248. height: number
  249. ): number {
  250. if (!levels || !levels.length) {
  251. return -1;
  252. }
  253.  
  254. // Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next
  255. // to determine whether we've chosen the greatest bandwidth for the media's dimensions
  256. const atGreatestBandwidth = (curLevel, nextLevel) => {
  257. if (!nextLevel) {
  258. return true;
  259. }
  260.  
  261. return (
  262. curLevel.width !== nextLevel.width ||
  263. curLevel.height !== nextLevel.height
  264. );
  265. };
  266.  
  267. // If we run through the loop without breaking, the media's dimensions are greater than every level, so default to
  268. // the max level
  269. let maxLevelIndex = levels.length - 1;
  270.  
  271. for (let i = 0; i < levels.length; i += 1) {
  272. const level = levels[i];
  273. if (
  274. (level.width >= width || level.height >= height) &&
  275. atGreatestBandwidth(level, levels[i + 1])
  276. ) {
  277. maxLevelIndex = i;
  278. break;
  279. }
  280. }
  281.  
  282. return maxLevelIndex;
  283. }
  284. }
  285.  
  286. export default CapLevelController;