Skip to content

Commit e6b01d8

Browse files
committed
Use ResizeObserver to cap level to player size
1 parent b7e1f4d commit e6b01d8

File tree

2 files changed

+90
-47
lines changed

2 files changed

+90
-47
lines changed

src/controller/cap-level-controller.ts

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,20 @@ import type { Level } from '../types/level';
1717

1818
type RestrictedLevel = { width: number; height: number; bitrate: number };
1919
class CapLevelController implements ComponentAPI {
20-
private hls: Hls;
20+
private hls: Hls | null = null;
2121
private autoLevelCapping: number;
22-
private firstLevel: number;
2322
private media: HTMLVideoElement | null;
2423
private restrictedLevels: RestrictedLevel[];
25-
private timer: number | undefined;
24+
private timer?: number;
25+
private observer?: ResizeObserver;
2626
private clientRect: { width: number; height: number } | null;
2727
private streamController?: StreamController;
2828

2929
constructor(hls: Hls) {
3030
this.hls = hls;
3131
this.autoLevelCapping = Number.POSITIVE_INFINITY;
32-
this.firstLevel = -1;
3332
this.media = null;
3433
this.restrictedLevels = [];
35-
this.timer = undefined;
3634
this.clientRect = null;
3735

3836
this.registerListeners();
@@ -46,39 +44,45 @@ class CapLevelController implements ComponentAPI {
4644
if (this.hls) {
4745
this.unregisterListener();
4846
}
49-
if (this.timer) {
47+
if (this.timer || this.observer) {
5048
this.stopCapping();
5149
}
52-
this.media = null;
53-
this.clientRect = null;
50+
this.media = this.clientRect = this.hls = null;
5451
// @ts-ignore
55-
this.hls = this.streamController = null;
52+
this.streamController = undefined;
5653
}
5754

5855
protected registerListeners() {
5956
const { hls } = this;
60-
hls.on(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
61-
hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
62-
hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
63-
hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
64-
hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this);
65-
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
57+
if (hls) {
58+
hls.on(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
59+
hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
60+
hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
61+
hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
62+
hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this);
63+
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
64+
}
6665
}
6766

6867
protected unregisterListener() {
6968
const { hls } = this;
70-
hls.off(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
71-
hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
72-
hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
73-
hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
74-
hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
75-
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
69+
if (hls) {
70+
hls.off(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
71+
hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
72+
hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
73+
hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
74+
hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
75+
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
76+
}
7677
}
7778

7879
protected onFpsDropLevelCapping(
7980
event: Events.FPS_DROP_LEVEL_CAPPING,
8081
data: FPSDropLevelCappingData,
8182
) {
83+
if (!this.hls) {
84+
return;
85+
}
8286
// Don't add a restricted level more than once
8387
const level = this.hls.levels[data.droppedLevel];
8488
if (this.isLevelAllowed(level)) {
@@ -94,9 +98,20 @@ class CapLevelController implements ComponentAPI {
9498
event: Events.MEDIA_ATTACHING,
9599
data: MediaAttachingData,
96100
) {
97-
this.media = data.media instanceof HTMLVideoElement ? data.media : null;
101+
const media = data.media;
98102
this.clientRect = null;
99-
if (this.timer && this.hls.levels.length) {
103+
if (!this.hls) {
104+
return;
105+
}
106+
if (media instanceof HTMLVideoElement) {
107+
this.media = media;
108+
if (this.hls.config.capLevelToPlayerSize) {
109+
this.observe();
110+
}
111+
} else {
112+
this.media = null;
113+
}
114+
if ((this.timer || this.observer) && this.hls.levels.length) {
100115
this.detectPlayerSize();
101116
}
102117
}
@@ -107,8 +122,7 @@ class CapLevelController implements ComponentAPI {
107122
) {
108123
const hls = this.hls;
109124
this.restrictedLevels = [];
110-
this.firstLevel = data.firstLevel;
111-
if (hls.config.capLevelToPlayerSize && data.video) {
125+
if (hls?.config.capLevelToPlayerSize && data.video) {
112126
// Start capping immediately if the manifest has signaled video codecs
113127
this.startCapping();
114128
}
@@ -118,7 +132,10 @@ class CapLevelController implements ComponentAPI {
118132
event: Events.LEVELS_UPDATED,
119133
data: LevelsUpdatedData,
120134
) {
121-
if (this.timer && Number.isFinite(this.autoLevelCapping)) {
135+
if (
136+
(this.timer || this.observer) &&
137+
Number.isFinite(this.autoLevelCapping)
138+
) {
122139
this.detectPlayerSize();
123140
}
124141
}
@@ -130,7 +147,7 @@ class CapLevelController implements ComponentAPI {
130147
data: BufferCodecsData,
131148
) {
132149
const hls = this.hls;
133-
if (hls.config.capLevelToPlayerSize && data.video) {
150+
if (hls?.config.capLevelToPlayerSize && data.video) {
134151
// If the manifest did not signal a video codec capping has been deferred until we're certain video is present
135152
this.startCapping();
136153
}
@@ -143,7 +160,7 @@ class CapLevelController implements ComponentAPI {
143160

144161
detectPlayerSize() {
145162
if (this.media) {
146-
if (this.mediaHeight <= 0 || this.mediaWidth <= 0) {
163+
if (this.mediaHeight <= 0 || this.mediaWidth <= 0 || !this.hls) {
147164
this.clientRect = null;
148165
return;
149166
}
@@ -175,6 +192,9 @@ class CapLevelController implements ComponentAPI {
175192
* returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
176193
*/
177194
getMaxLevel(capLevelIndex: number): number {
195+
if (!this.hls) {
196+
return -1;
197+
}
178198
const levels = this.hls.levels;
179199
if (!levels.length) {
180200
return -1;
@@ -183,34 +203,58 @@ class CapLevelController implements ComponentAPI {
183203
const validLevels = levels.filter(
184204
(level, index) => this.isLevelAllowed(level) && index <= capLevelIndex,
185205
);
186-
187-
this.clientRect = null;
206+
if (!this.observer) {
207+
this.clientRect = null;
208+
}
188209
return CapLevelController.getMaxLevelByMediaSize(
189210
validLevels,
190211
this.mediaWidth,
191212
this.mediaHeight,
192213
);
193214
}
194215

216+
private observe() {
217+
const ResizeObserver = self.ResizeObserver;
218+
if (ResizeObserver) {
219+
this.observer = new ResizeObserver((entries) => {
220+
const bounds = entries[0]?.contentRect;
221+
if (bounds) {
222+
this.clientRect = bounds;
223+
this.detectPlayerSize();
224+
}
225+
});
226+
}
227+
if (this.observer && this.media) {
228+
this.observer.observe(this.media);
229+
}
230+
}
231+
195232
startCapping() {
196-
if (this.timer) {
233+
if (this.timer || this.observer) {
197234
// Don't reset capping if started twice; this can happen if the manifest signals a video codec
198235
return;
199236
}
200-
this.autoLevelCapping = Number.POSITIVE_INFINITY;
201237
self.clearInterval(this.timer);
202-
this.timer = self.setInterval(this.detectPlayerSize.bind(this), 1000);
238+
this.timer = undefined;
239+
this.autoLevelCapping = Number.POSITIVE_INFINITY;
240+
this.observe();
241+
if (!this.observer) {
242+
this.timer = self.setInterval(this.detectPlayerSize.bind(this), 1000);
243+
}
203244
this.detectPlayerSize();
204245
}
205246

206247
stopCapping() {
207248
this.restrictedLevels = [];
208-
this.firstLevel = -1;
209249
this.autoLevelCapping = Number.POSITIVE_INFINITY;
210250
if (this.timer) {
211251
self.clearInterval(this.timer);
212252
this.timer = undefined;
213253
}
254+
if (this.observer) {
255+
this.observer.disconnect();
256+
this.observer = undefined;
257+
}
214258
}
215259

216260
getDimensions(): { width: number; height: number } {
@@ -250,15 +294,18 @@ class CapLevelController implements ComponentAPI {
250294

251295
get contentScaleFactor(): number {
252296
let pixelRatio = 1;
253-
if (!this.hls.config.ignoreDevicePixelRatio) {
297+
if (!this.hls) {
298+
return pixelRatio;
299+
}
300+
const { ignoreDevicePixelRatio, maxDevicePixelRatio } = this.hls.config;
301+
if (!ignoreDevicePixelRatio) {
254302
try {
255303
pixelRatio = self.devicePixelRatio;
256304
} catch (e) {
257305
/* no-op */
258306
}
259307
}
260-
261-
return Math.min(pixelRatio, this.hls.config.maxDevicePixelRatio);
308+
return Math.min(pixelRatio, maxDevicePixelRatio);
262309
}
263310

264311
private isLevelAllowed(level: Level): boolean {

tests/unit/controller/cap-level-controller.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,15 @@ describe('CapLevelController', function () {
195195
});
196196

197197
describe('start and stop', function () {
198-
it('immediately caps and sets a timer for monitoring size size', function () {
198+
it('immediately caps and begins monitoring size', function () {
199199
const detectPlayerSizeSpy = sinon.spy(
200200
capLevelController,
201201
'detectPlayerSize',
202202
);
203203
capLevelController.startCapping();
204204

205-
expect(capLevelController.timer).to.exist;
205+
expect(capLevelController.timer || capLevelController.observer).to
206+
.exist;
206207
expect(detectPlayerSizeSpy.callCount).to.equal(1);
207208
});
208209

@@ -215,7 +216,6 @@ describe('CapLevelController', function () {
215216
Number.POSITIVE_INFINITY,
216217
);
217218
expect(capLevelController.restrictedLevels).to.be.empty;
218-
expect(capLevelController.firstLevel).to.equal(-1);
219219
expect(capLevelController.timer).to.not.exist;
220220
});
221221
});
@@ -246,15 +246,11 @@ describe('CapLevelController', function () {
246246
expect(startCappingSpy.calledOnce).to.be.true;
247247
});
248248

249-
it('receives level information from the MANIFEST_PARSED event', function () {
249+
it('resets restrictedLevels on MANIFEST_PARSED', function () {
250250
capLevelController.restrictedLevels = [1];
251-
const data = {
251+
capLevelController.onManifestParsed(Events.MANIFEST_PARSED, {
252252
levels: [{ foo: 'bar' }],
253-
firstLevel: 0,
254-
};
255-
256-
capLevelController.onManifestParsed(Events.MANIFEST_PARSED, data);
257-
expect(capLevelController.firstLevel).to.equal(data.firstLevel);
253+
});
258254
expect(capLevelController.restrictedLevels).to.be.empty;
259255
});
260256

0 commit comments

Comments
 (0)