@zenvor/hls.js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BufferOperation,
|
|
3
|
+
BufferOperationQueues,
|
|
4
|
+
SourceBufferName,
|
|
5
|
+
SourceBufferTrackSet,
|
|
6
|
+
} from '../types/buffer';
|
|
7
|
+
|
|
8
|
+
export default class BufferOperationQueue {
|
|
9
|
+
private tracks: SourceBufferTrackSet | null;
|
|
10
|
+
private queues: BufferOperationQueues | null = {
|
|
11
|
+
video: [],
|
|
12
|
+
audio: [],
|
|
13
|
+
audiovideo: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
constructor(sourceBufferReference: SourceBufferTrackSet) {
|
|
17
|
+
this.tracks = sourceBufferReference;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public destroy() {
|
|
21
|
+
this.tracks = this.queues = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public append(
|
|
25
|
+
operation: BufferOperation,
|
|
26
|
+
type: SourceBufferName,
|
|
27
|
+
pending?: boolean | undefined,
|
|
28
|
+
) {
|
|
29
|
+
if (this.queues === null || this.tracks === null) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const queue = this.queues[type];
|
|
33
|
+
queue.push(operation);
|
|
34
|
+
if (queue.length === 1 && !pending) {
|
|
35
|
+
this.executeNext(type);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public appendBlocker(type: SourceBufferName): Promise<void> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const operation: BufferOperation = {
|
|
42
|
+
label: 'async-blocker',
|
|
43
|
+
execute: resolve,
|
|
44
|
+
onStart: () => {},
|
|
45
|
+
onComplete: () => {},
|
|
46
|
+
onError: () => {},
|
|
47
|
+
};
|
|
48
|
+
this.append(operation, type);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public prependBlocker(type: SourceBufferName): Promise<void> {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
if (this.queues) {
|
|
55
|
+
const operation: BufferOperation = {
|
|
56
|
+
label: 'async-blocker-prepend',
|
|
57
|
+
execute: resolve,
|
|
58
|
+
onStart: () => {},
|
|
59
|
+
onComplete: () => {},
|
|
60
|
+
onError: () => {},
|
|
61
|
+
};
|
|
62
|
+
this.queues[type].unshift(operation);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public removeBlockers() {
|
|
68
|
+
if (this.queues === null) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
[this.queues.video, this.queues.audio, this.queues.audiovideo].forEach(
|
|
72
|
+
(queue) => {
|
|
73
|
+
const label = queue[0]?.label;
|
|
74
|
+
if (label === 'async-blocker' || label === 'async-blocker-prepend') {
|
|
75
|
+
queue[0].execute();
|
|
76
|
+
queue.splice(0, 1);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public unblockAudio(op: BufferOperation) {
|
|
83
|
+
if (this.queues === null) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const queue = this.queues.audio;
|
|
87
|
+
if (queue[0] === op) {
|
|
88
|
+
this.shiftAndExecuteNext('audio');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public executeNext(type: SourceBufferName) {
|
|
93
|
+
if (this.queues === null || this.tracks === null) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const queue = this.queues[type];
|
|
97
|
+
if (queue.length) {
|
|
98
|
+
const operation: BufferOperation = queue[0];
|
|
99
|
+
try {
|
|
100
|
+
// Operations are expected to result in an 'updateend' event being fired. If not, the queue will lock. Operations
|
|
101
|
+
// which do not end with this event must call _onSBUpdateEnd manually
|
|
102
|
+
operation.execute();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
operation.onError(error);
|
|
105
|
+
if (this.queues === null || this.tracks === null) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Only shift the current operation off, otherwise the updateend handler will do this for us
|
|
110
|
+
const sb = this.tracks[type]?.buffer;
|
|
111
|
+
if (!sb?.updating) {
|
|
112
|
+
this.shiftAndExecuteNext(type);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public shiftAndExecuteNext(type: SourceBufferName) {
|
|
119
|
+
if (this.queues === null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.queues[type].shift();
|
|
123
|
+
this.executeNext(type);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public current(type: SourceBufferName): BufferOperation | null {
|
|
127
|
+
return this.queues?.[type][0] || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public toString(): string {
|
|
131
|
+
const { queues, tracks } = this;
|
|
132
|
+
if (queues === null || tracks === null) {
|
|
133
|
+
return `<destroyed>`;
|
|
134
|
+
}
|
|
135
|
+
return `
|
|
136
|
+
${this.list('video')}
|
|
137
|
+
${this.list('audio')}
|
|
138
|
+
${this.list('audiovideo')}}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public list(type: SourceBufferName): string {
|
|
142
|
+
return this.queues?.[type] || this.tracks?.[type]
|
|
143
|
+
? `${type}: (${this.listSbInfo(type)}) ${this.listOps(type)}`
|
|
144
|
+
: '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private listSbInfo(type: SourceBufferName): string {
|
|
148
|
+
const track = this.tracks?.[type];
|
|
149
|
+
const sb = track?.buffer;
|
|
150
|
+
if (!sb) {
|
|
151
|
+
return 'none';
|
|
152
|
+
}
|
|
153
|
+
return `SourceBuffer${sb.updating ? ' updating' : ''}${track.ended ? ' ended' : ''}${track.ending ? ' ending' : ''}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private listOps(type: SourceBufferName): string {
|
|
157
|
+
return this.queues?.[type].map((op) => op.label).join(', ') || '';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* cap stream level to media size dimension controller
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Events } from '../events';
|
|
6
|
+
import type StreamController from './stream-controller';
|
|
7
|
+
import type Hls from '../hls';
|
|
8
|
+
import type { ComponentAPI } from '../types/component-api';
|
|
9
|
+
import type {
|
|
10
|
+
BufferCodecsData,
|
|
11
|
+
FPSDropLevelCappingData,
|
|
12
|
+
LevelsUpdatedData,
|
|
13
|
+
ManifestParsedData,
|
|
14
|
+
MediaAttachingData,
|
|
15
|
+
} from '../types/events';
|
|
16
|
+
import type { Level } from '../types/level';
|
|
17
|
+
|
|
18
|
+
type RestrictedLevel = { width: number; height: number; bitrate: number };
|
|
19
|
+
class CapLevelController implements ComponentAPI {
|
|
20
|
+
private hls: Hls | null = null;
|
|
21
|
+
private autoLevelCapping: number;
|
|
22
|
+
private media: HTMLVideoElement | null;
|
|
23
|
+
private restrictedLevels: RestrictedLevel[];
|
|
24
|
+
private timer?: number;
|
|
25
|
+
private observer?: ResizeObserver;
|
|
26
|
+
private clientRect: { width: number; height: number } | null;
|
|
27
|
+
private streamController?: StreamController;
|
|
28
|
+
|
|
29
|
+
constructor(hls: Hls) {
|
|
30
|
+
this.hls = hls;
|
|
31
|
+
this.autoLevelCapping = Number.POSITIVE_INFINITY;
|
|
32
|
+
this.media = null;
|
|
33
|
+
this.restrictedLevels = [];
|
|
34
|
+
this.clientRect = null;
|
|
35
|
+
|
|
36
|
+
this.registerListeners();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public setStreamController(streamController: StreamController) {
|
|
40
|
+
this.streamController = streamController;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public destroy() {
|
|
44
|
+
if (this.hls) {
|
|
45
|
+
this.unregisterListener();
|
|
46
|
+
}
|
|
47
|
+
if (this.timer || this.observer) {
|
|
48
|
+
this.stopCapping();
|
|
49
|
+
}
|
|
50
|
+
this.media = this.clientRect = this.hls = null;
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
this.streamController = undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected registerListeners() {
|
|
56
|
+
const { hls } = 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
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected unregisterListener() {
|
|
68
|
+
const { hls } = 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
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected onFpsDropLevelCapping(
|
|
80
|
+
event: Events.FPS_DROP_LEVEL_CAPPING,
|
|
81
|
+
data: FPSDropLevelCappingData,
|
|
82
|
+
) {
|
|
83
|
+
if (!this.hls) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Don't add a restricted level more than once
|
|
87
|
+
const level = this.hls.levels[data.droppedLevel];
|
|
88
|
+
if (this.isLevelAllowed(level)) {
|
|
89
|
+
this.restrictedLevels.push({
|
|
90
|
+
bitrate: level.bitrate,
|
|
91
|
+
height: level.height,
|
|
92
|
+
width: level.width,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected onMediaAttaching(
|
|
98
|
+
event: Events.MEDIA_ATTACHING,
|
|
99
|
+
data: MediaAttachingData,
|
|
100
|
+
) {
|
|
101
|
+
const media = data.media;
|
|
102
|
+
this.clientRect = null;
|
|
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) {
|
|
115
|
+
this.detectPlayerSize();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected onManifestParsed(
|
|
120
|
+
event: Events.MANIFEST_PARSED,
|
|
121
|
+
data: ManifestParsedData,
|
|
122
|
+
) {
|
|
123
|
+
const hls = this.hls;
|
|
124
|
+
this.restrictedLevels = [];
|
|
125
|
+
if (hls?.config.capLevelToPlayerSize && data.video) {
|
|
126
|
+
// Start capping immediately if the manifest has signaled video codecs
|
|
127
|
+
this.startCapping();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private onLevelsUpdated(
|
|
132
|
+
event: Events.LEVELS_UPDATED,
|
|
133
|
+
data: LevelsUpdatedData,
|
|
134
|
+
) {
|
|
135
|
+
if (
|
|
136
|
+
(this.timer || this.observer) &&
|
|
137
|
+
Number.isFinite(this.autoLevelCapping)
|
|
138
|
+
) {
|
|
139
|
+
this.detectPlayerSize();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted
|
|
144
|
+
// to the first level
|
|
145
|
+
protected onBufferCodecs(
|
|
146
|
+
event: Events.BUFFER_CODECS,
|
|
147
|
+
data: BufferCodecsData,
|
|
148
|
+
) {
|
|
149
|
+
const hls = this.hls;
|
|
150
|
+
if (hls?.config.capLevelToPlayerSize && data.video) {
|
|
151
|
+
// If the manifest did not signal a video codec capping has been deferred until we're certain video is present
|
|
152
|
+
this.startCapping();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected onMediaDetaching() {
|
|
157
|
+
this.stopCapping();
|
|
158
|
+
this.media = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
detectPlayerSize() {
|
|
162
|
+
if (this.media) {
|
|
163
|
+
if (this.mediaHeight <= 0 || this.mediaWidth <= 0 || !this.hls) {
|
|
164
|
+
this.clientRect = null;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const levels = this.hls.levels;
|
|
168
|
+
if (levels.length) {
|
|
169
|
+
const hls = this.hls;
|
|
170
|
+
const maxLevel = this.getMaxLevel(levels.length - 1);
|
|
171
|
+
if (maxLevel !== this.autoLevelCapping) {
|
|
172
|
+
hls.logger.log(
|
|
173
|
+
`Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
hls.autoLevelCapping = maxLevel;
|
|
177
|
+
if (
|
|
178
|
+
hls.autoLevelEnabled &&
|
|
179
|
+
hls.autoLevelCapping > this.autoLevelCapping &&
|
|
180
|
+
this.streamController
|
|
181
|
+
) {
|
|
182
|
+
// if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch
|
|
183
|
+
// usually happen when the user go to the fullscreen mode.
|
|
184
|
+
this.streamController.nextLevelSwitch();
|
|
185
|
+
}
|
|
186
|
+
this.autoLevelCapping = hls.autoLevelCapping;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
|
|
193
|
+
*/
|
|
194
|
+
getMaxLevel(capLevelIndex: number): number {
|
|
195
|
+
if (!this.hls) {
|
|
196
|
+
return -1;
|
|
197
|
+
}
|
|
198
|
+
const levels = this.hls.levels;
|
|
199
|
+
if (!levels.length) {
|
|
200
|
+
return -1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const validLevels = levels.filter(
|
|
204
|
+
(level, index) => this.isLevelAllowed(level) && index <= capLevelIndex,
|
|
205
|
+
);
|
|
206
|
+
if (!this.observer) {
|
|
207
|
+
this.clientRect = null;
|
|
208
|
+
}
|
|
209
|
+
return CapLevelController.getMaxLevelByMediaSize(
|
|
210
|
+
validLevels,
|
|
211
|
+
this.mediaWidth,
|
|
212
|
+
this.mediaHeight,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
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
|
+
|
|
232
|
+
startCapping() {
|
|
233
|
+
if (this.timer || this.observer) {
|
|
234
|
+
// Don't reset capping if started twice; this can happen if the manifest signals a video codec
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
self.clearInterval(this.timer);
|
|
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
|
+
}
|
|
244
|
+
this.detectPlayerSize();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
stopCapping() {
|
|
248
|
+
this.restrictedLevels = [];
|
|
249
|
+
this.autoLevelCapping = Number.POSITIVE_INFINITY;
|
|
250
|
+
if (this.timer) {
|
|
251
|
+
self.clearInterval(this.timer);
|
|
252
|
+
this.timer = undefined;
|
|
253
|
+
}
|
|
254
|
+
if (this.observer) {
|
|
255
|
+
this.observer.disconnect();
|
|
256
|
+
this.observer = undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getDimensions(): { width: number; height: number } {
|
|
261
|
+
if (this.clientRect) {
|
|
262
|
+
return this.clientRect;
|
|
263
|
+
}
|
|
264
|
+
const media = this.media;
|
|
265
|
+
const boundsRect = {
|
|
266
|
+
width: 0,
|
|
267
|
+
height: 0,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (media) {
|
|
271
|
+
const clientRect = media.getBoundingClientRect();
|
|
272
|
+
boundsRect.width = clientRect.width;
|
|
273
|
+
boundsRect.height = clientRect.height;
|
|
274
|
+
if (!boundsRect.width && !boundsRect.height) {
|
|
275
|
+
// When the media element has no width or height (equivalent to not being in the DOM),
|
|
276
|
+
// then use its width and height attributes (media.width, media.height)
|
|
277
|
+
boundsRect.width =
|
|
278
|
+
clientRect.right - clientRect.left || media.width || 0;
|
|
279
|
+
boundsRect.height =
|
|
280
|
+
clientRect.bottom - clientRect.top || media.height || 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.clientRect = boundsRect;
|
|
284
|
+
return boundsRect;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get mediaWidth(): number {
|
|
288
|
+
return this.getDimensions().width * this.contentScaleFactor;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get mediaHeight(): number {
|
|
292
|
+
return this.getDimensions().height * this.contentScaleFactor;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get contentScaleFactor(): number {
|
|
296
|
+
let pixelRatio = 1;
|
|
297
|
+
if (!this.hls) {
|
|
298
|
+
return pixelRatio;
|
|
299
|
+
}
|
|
300
|
+
const { ignoreDevicePixelRatio, maxDevicePixelRatio } = this.hls.config;
|
|
301
|
+
if (!ignoreDevicePixelRatio) {
|
|
302
|
+
try {
|
|
303
|
+
pixelRatio = self.devicePixelRatio;
|
|
304
|
+
} catch (e) {
|
|
305
|
+
/* no-op */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return Math.min(pixelRatio, maxDevicePixelRatio);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private isLevelAllowed(level: Level): boolean {
|
|
312
|
+
const restrictedLevels = this.restrictedLevels;
|
|
313
|
+
return !restrictedLevels.some((restrictedLevel) => {
|
|
314
|
+
return (
|
|
315
|
+
level.bitrate === restrictedLevel.bitrate &&
|
|
316
|
+
level.width === restrictedLevel.width &&
|
|
317
|
+
level.height === restrictedLevel.height
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
static getMaxLevelByMediaSize(
|
|
323
|
+
levels: Array<Level>,
|
|
324
|
+
width: number,
|
|
325
|
+
height: number,
|
|
326
|
+
): number {
|
|
327
|
+
if (!levels?.length) {
|
|
328
|
+
return -1;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next
|
|
332
|
+
// to determine whether we've chosen the greatest bandwidth for the media's dimensions
|
|
333
|
+
const atGreatestBandwidth = (
|
|
334
|
+
curLevel: Level,
|
|
335
|
+
nextLevel: Level | undefined,
|
|
336
|
+
) => {
|
|
337
|
+
if (!nextLevel) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
curLevel.width !== nextLevel.width ||
|
|
343
|
+
curLevel.height !== nextLevel.height
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// If we run through the loop without breaking, the media's dimensions are greater than every level, so default to
|
|
348
|
+
// the max level
|
|
349
|
+
let maxLevelIndex = levels.length - 1;
|
|
350
|
+
// Prevent changes in aspect-ratio from causing capping to toggle back and forth
|
|
351
|
+
const squareSize = Math.max(width, height);
|
|
352
|
+
for (let i = 0; i < levels.length; i += 1) {
|
|
353
|
+
const level = levels[i];
|
|
354
|
+
if (
|
|
355
|
+
(level.width >= squareSize || level.height >= squareSize) &&
|
|
356
|
+
atGreatestBandwidth(level, levels[i + 1])
|
|
357
|
+
) {
|
|
358
|
+
maxLevelIndex = i;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return maxLevelIndex;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export default CapLevelController;
|