@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,26 @@
|
|
|
1
|
+
export function base64ToBase64Url(base64encodedStr: string): string {
|
|
2
|
+
return base64encodedStr
|
|
3
|
+
.replace(/\+/g, '-')
|
|
4
|
+
.replace(/\//g, '_')
|
|
5
|
+
.replace(/=+$/, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function strToBase64Encode(str: string): string {
|
|
9
|
+
return btoa(str);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function base64DecodeToStr(str: string): string {
|
|
13
|
+
return atob(str);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function base64Encode(input: Uint8Array): string {
|
|
17
|
+
return btoa(String.fromCharCode(...input));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function base64UrlEncode(input: Uint8Array): string {
|
|
21
|
+
return base64ToBase64Url(base64Encode(input));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function base64Decode(base64encodedStr: string) {
|
|
25
|
+
return Uint8Array.from(atob(base64encodedStr), (c) => c.charCodeAt(0));
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CaptionScreen } from './cea-608-parser';
|
|
2
|
+
import type { TimelineController } from '../controller/timeline-controller';
|
|
3
|
+
|
|
4
|
+
export default class OutputFilter {
|
|
5
|
+
private timelineController: TimelineController;
|
|
6
|
+
private cueRanges: Array<[number, number]> = [];
|
|
7
|
+
private trackName: string;
|
|
8
|
+
private startTime: number | null = null;
|
|
9
|
+
private endTime: number | null = null;
|
|
10
|
+
private screen: CaptionScreen | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(timelineController: TimelineController, trackName: string) {
|
|
13
|
+
this.timelineController = timelineController;
|
|
14
|
+
this.trackName = trackName;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
dispatchCue() {
|
|
18
|
+
if (this.startTime === null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.timelineController.addCues(
|
|
23
|
+
this.trackName,
|
|
24
|
+
this.startTime,
|
|
25
|
+
this.endTime as number,
|
|
26
|
+
this.screen as CaptionScreen,
|
|
27
|
+
this.cueRanges,
|
|
28
|
+
);
|
|
29
|
+
this.startTime = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
newCue(startTime: number, endTime: number, screen: CaptionScreen) {
|
|
33
|
+
if (this.startTime === null || this.startTime > startTime) {
|
|
34
|
+
this.startTime = startTime;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.endTime = endTime;
|
|
38
|
+
this.screen = screen;
|
|
39
|
+
this.timelineController.createCaptionsTrack(this.trackName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reset() {
|
|
43
|
+
this.cueRanges = [];
|
|
44
|
+
this.startTime = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { codecsSetSelectionPreferenceValue } from './codecs';
|
|
2
|
+
import { getVideoSelectionOptions } from './hdr';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
import { stringify } from './safe-json-stringify';
|
|
5
|
+
import type Hls from '../hls';
|
|
6
|
+
import type { Level, VideoRange } from '../types/level';
|
|
7
|
+
import type {
|
|
8
|
+
AudioSelectionOption,
|
|
9
|
+
MediaPlaylist,
|
|
10
|
+
SubtitleSelectionOption,
|
|
11
|
+
VideoSelectionOption,
|
|
12
|
+
} from '../types/media-playlist';
|
|
13
|
+
|
|
14
|
+
export type CodecSetTier = {
|
|
15
|
+
minBitrate: number;
|
|
16
|
+
minHeight: number;
|
|
17
|
+
minFramerate: number;
|
|
18
|
+
minIndex: number;
|
|
19
|
+
maxScore: number;
|
|
20
|
+
videoRanges: Record<string, number>;
|
|
21
|
+
channels: Record<string, number>;
|
|
22
|
+
hasDefaultAudio: boolean;
|
|
23
|
+
fragmentError: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type AudioTrackGroup = {
|
|
27
|
+
tracks: MediaPlaylist[];
|
|
28
|
+
channels: Record<string, number>;
|
|
29
|
+
hasDefault: boolean;
|
|
30
|
+
hasAutoSelect: boolean;
|
|
31
|
+
};
|
|
32
|
+
type StartParameters = {
|
|
33
|
+
codecSet: string | undefined;
|
|
34
|
+
videoRanges: Array<VideoRange>;
|
|
35
|
+
preferHDR: boolean;
|
|
36
|
+
minFramerate: number;
|
|
37
|
+
minBitrate: number;
|
|
38
|
+
minIndex: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function getStartCodecTier(
|
|
42
|
+
codecTiers: Record<string, CodecSetTier>,
|
|
43
|
+
currentVideoRange: VideoRange | undefined,
|
|
44
|
+
currentBw: number,
|
|
45
|
+
audioPreference: AudioSelectionOption | undefined,
|
|
46
|
+
videoPreference: VideoSelectionOption | undefined,
|
|
47
|
+
): StartParameters {
|
|
48
|
+
const codecSets = Object.keys(codecTiers);
|
|
49
|
+
const channelsPreference = audioPreference?.channels;
|
|
50
|
+
const audioCodecPreference = audioPreference?.audioCodec;
|
|
51
|
+
const videoCodecPreference = videoPreference?.videoCodec;
|
|
52
|
+
const preferStereo = channelsPreference && parseInt(channelsPreference) === 2;
|
|
53
|
+
// Use first level set to determine stereo, and minimum resolution and framerate
|
|
54
|
+
let hasStereo = false;
|
|
55
|
+
let hasCurrentVideoRange = false;
|
|
56
|
+
let minHeight = Infinity;
|
|
57
|
+
let minFramerate = Infinity;
|
|
58
|
+
let minBitrate = Infinity;
|
|
59
|
+
let minIndex = Infinity;
|
|
60
|
+
let selectedScore = 0;
|
|
61
|
+
let videoRanges: Array<VideoRange> = [];
|
|
62
|
+
|
|
63
|
+
const { preferHDR, allowedVideoRanges } = getVideoSelectionOptions(
|
|
64
|
+
currentVideoRange,
|
|
65
|
+
videoPreference,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
for (let i = codecSets.length; i--; ) {
|
|
69
|
+
const tier = codecTiers[codecSets[i]];
|
|
70
|
+
hasStereo ||= tier.channels[2] > 0;
|
|
71
|
+
minHeight = Math.min(minHeight, tier.minHeight);
|
|
72
|
+
minFramerate = Math.min(minFramerate, tier.minFramerate);
|
|
73
|
+
minBitrate = Math.min(minBitrate, tier.minBitrate);
|
|
74
|
+
const matchingVideoRanges = allowedVideoRanges.filter(
|
|
75
|
+
(range) => tier.videoRanges[range] > 0,
|
|
76
|
+
);
|
|
77
|
+
if (matchingVideoRanges.length > 0) {
|
|
78
|
+
hasCurrentVideoRange = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
minHeight = Number.isFinite(minHeight) ? minHeight : 0;
|
|
82
|
+
minFramerate = Number.isFinite(minFramerate) ? minFramerate : 0;
|
|
83
|
+
const maxHeight = Math.max(1080, minHeight);
|
|
84
|
+
const maxFramerate = Math.max(30, minFramerate);
|
|
85
|
+
minBitrate = Number.isFinite(minBitrate) ? minBitrate : currentBw;
|
|
86
|
+
currentBw = Math.max(minBitrate, currentBw);
|
|
87
|
+
// If there are no variants with matching preference, set currentVideoRange to undefined
|
|
88
|
+
if (!hasCurrentVideoRange) {
|
|
89
|
+
currentVideoRange = undefined;
|
|
90
|
+
}
|
|
91
|
+
const hasMultipleSets = codecSets.length > 1;
|
|
92
|
+
const codecSet = codecSets.reduce(
|
|
93
|
+
(selected: string | undefined, candidate: string) => {
|
|
94
|
+
// Remove candiates which do not meet bitrate, default audio, stereo or channels preference, 1080p or lower, 30fps or lower, or SDR/HDR selection if present
|
|
95
|
+
const candidateTier = codecTiers[candidate];
|
|
96
|
+
if (candidate === selected) {
|
|
97
|
+
return selected;
|
|
98
|
+
}
|
|
99
|
+
videoRanges = hasCurrentVideoRange
|
|
100
|
+
? allowedVideoRanges.filter(
|
|
101
|
+
(range) => candidateTier.videoRanges[range] > 0,
|
|
102
|
+
)
|
|
103
|
+
: [];
|
|
104
|
+
if (hasMultipleSets) {
|
|
105
|
+
if (candidateTier.minBitrate > currentBw) {
|
|
106
|
+
logStartCodecCandidateIgnored(
|
|
107
|
+
candidate,
|
|
108
|
+
`min bitrate of ${candidateTier.minBitrate} > current estimate of ${currentBw}`,
|
|
109
|
+
);
|
|
110
|
+
return selected;
|
|
111
|
+
}
|
|
112
|
+
if (!candidateTier.hasDefaultAudio) {
|
|
113
|
+
logStartCodecCandidateIgnored(
|
|
114
|
+
candidate,
|
|
115
|
+
`no renditions with default or auto-select sound found`,
|
|
116
|
+
);
|
|
117
|
+
return selected;
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
audioCodecPreference &&
|
|
121
|
+
candidate.indexOf(audioCodecPreference.substring(0, 4)) % 5 !== 0
|
|
122
|
+
) {
|
|
123
|
+
logStartCodecCandidateIgnored(
|
|
124
|
+
candidate,
|
|
125
|
+
`audio codec preference "${audioCodecPreference}" not found`,
|
|
126
|
+
);
|
|
127
|
+
return selected;
|
|
128
|
+
}
|
|
129
|
+
if (channelsPreference && !preferStereo) {
|
|
130
|
+
if (!candidateTier.channels[channelsPreference]) {
|
|
131
|
+
logStartCodecCandidateIgnored(
|
|
132
|
+
candidate,
|
|
133
|
+
`no renditions with ${channelsPreference} channel sound found (channels options: ${Object.keys(
|
|
134
|
+
candidateTier.channels,
|
|
135
|
+
)})`,
|
|
136
|
+
);
|
|
137
|
+
return selected;
|
|
138
|
+
}
|
|
139
|
+
} else if (
|
|
140
|
+
(!audioCodecPreference || preferStereo) &&
|
|
141
|
+
hasStereo &&
|
|
142
|
+
candidateTier.channels['2'] === 0
|
|
143
|
+
) {
|
|
144
|
+
logStartCodecCandidateIgnored(
|
|
145
|
+
candidate,
|
|
146
|
+
`no renditions with stereo sound found`,
|
|
147
|
+
);
|
|
148
|
+
return selected;
|
|
149
|
+
}
|
|
150
|
+
if (candidateTier.minHeight > maxHeight) {
|
|
151
|
+
logStartCodecCandidateIgnored(
|
|
152
|
+
candidate,
|
|
153
|
+
`min resolution of ${candidateTier.minHeight} > maximum of ${maxHeight}`,
|
|
154
|
+
);
|
|
155
|
+
return selected;
|
|
156
|
+
}
|
|
157
|
+
if (candidateTier.minFramerate > maxFramerate) {
|
|
158
|
+
logStartCodecCandidateIgnored(
|
|
159
|
+
candidate,
|
|
160
|
+
`min framerate of ${candidateTier.minFramerate} > maximum of ${maxFramerate}`,
|
|
161
|
+
);
|
|
162
|
+
return selected;
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
!videoRanges.some((range) => candidateTier.videoRanges[range] > 0)
|
|
166
|
+
) {
|
|
167
|
+
logStartCodecCandidateIgnored(
|
|
168
|
+
candidate,
|
|
169
|
+
`no variants with VIDEO-RANGE of ${stringify(videoRanges)} found`,
|
|
170
|
+
);
|
|
171
|
+
return selected;
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
videoCodecPreference &&
|
|
175
|
+
candidate.indexOf(videoCodecPreference.substring(0, 4)) % 5 !== 0
|
|
176
|
+
) {
|
|
177
|
+
logStartCodecCandidateIgnored(
|
|
178
|
+
candidate,
|
|
179
|
+
`video codec preference "${videoCodecPreference}" not found`,
|
|
180
|
+
);
|
|
181
|
+
return selected;
|
|
182
|
+
}
|
|
183
|
+
if (candidateTier.maxScore < selectedScore) {
|
|
184
|
+
logStartCodecCandidateIgnored(
|
|
185
|
+
candidate,
|
|
186
|
+
`max score of ${candidateTier.maxScore} < selected max of ${selectedScore}`,
|
|
187
|
+
);
|
|
188
|
+
return selected;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Remove candiates with less preferred codecs or more errors
|
|
192
|
+
if (
|
|
193
|
+
selected &&
|
|
194
|
+
(codecsSetSelectionPreferenceValue(candidate) >=
|
|
195
|
+
codecsSetSelectionPreferenceValue(selected) ||
|
|
196
|
+
candidateTier.fragmentError > codecTiers[selected].fragmentError)
|
|
197
|
+
) {
|
|
198
|
+
return selected;
|
|
199
|
+
}
|
|
200
|
+
minIndex = candidateTier.minIndex;
|
|
201
|
+
selectedScore = candidateTier.maxScore;
|
|
202
|
+
return candidate;
|
|
203
|
+
},
|
|
204
|
+
undefined,
|
|
205
|
+
);
|
|
206
|
+
return {
|
|
207
|
+
codecSet,
|
|
208
|
+
videoRanges,
|
|
209
|
+
preferHDR,
|
|
210
|
+
minFramerate,
|
|
211
|
+
minBitrate,
|
|
212
|
+
minIndex,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function logStartCodecCandidateIgnored(codeSet: string, reason: string) {
|
|
217
|
+
logger.log(
|
|
218
|
+
`[abr] start candidates with "${codeSet}" ignored because ${reason}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type AudioTracksByGroup = {
|
|
223
|
+
hasDefaultAudio: boolean;
|
|
224
|
+
hasAutoSelectAudio: boolean;
|
|
225
|
+
groups: Record<string, AudioTrackGroup>;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export function getAudioTracksByGroup(allAudioTracks: MediaPlaylist[]) {
|
|
229
|
+
return allAudioTracks.reduce(
|
|
230
|
+
(audioTracksByGroup: AudioTracksByGroup, track) => {
|
|
231
|
+
let trackGroup = audioTracksByGroup.groups[track.groupId];
|
|
232
|
+
if (!trackGroup) {
|
|
233
|
+
trackGroup = audioTracksByGroup.groups[track.groupId] = {
|
|
234
|
+
tracks: [],
|
|
235
|
+
channels: { 2: 0 },
|
|
236
|
+
hasDefault: false,
|
|
237
|
+
hasAutoSelect: false,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
trackGroup.tracks.push(track);
|
|
241
|
+
const channelsKey = track.channels || '2';
|
|
242
|
+
trackGroup.channels[channelsKey] =
|
|
243
|
+
(trackGroup.channels[channelsKey] || 0) + 1;
|
|
244
|
+
trackGroup.hasDefault = trackGroup.hasDefault || track.default;
|
|
245
|
+
trackGroup.hasAutoSelect = trackGroup.hasAutoSelect || track.autoselect;
|
|
246
|
+
if (trackGroup.hasDefault) {
|
|
247
|
+
audioTracksByGroup.hasDefaultAudio = true;
|
|
248
|
+
}
|
|
249
|
+
if (trackGroup.hasAutoSelect) {
|
|
250
|
+
audioTracksByGroup.hasAutoSelectAudio = true;
|
|
251
|
+
}
|
|
252
|
+
return audioTracksByGroup;
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
hasDefaultAudio: false,
|
|
256
|
+
hasAutoSelectAudio: false,
|
|
257
|
+
groups: {},
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function getCodecTiers(
|
|
263
|
+
levels: Level[],
|
|
264
|
+
audioTracksByGroup: AudioTracksByGroup,
|
|
265
|
+
minAutoLevel: number,
|
|
266
|
+
maxAutoLevel: number,
|
|
267
|
+
): Record<string, CodecSetTier> {
|
|
268
|
+
return levels
|
|
269
|
+
.slice(minAutoLevel, maxAutoLevel + 1)
|
|
270
|
+
.reduce((tiers: Record<string, CodecSetTier>, level, index) => {
|
|
271
|
+
if (!level.codecSet) {
|
|
272
|
+
return tiers;
|
|
273
|
+
}
|
|
274
|
+
const audioGroups = level.audioGroups;
|
|
275
|
+
let tier = tiers[level.codecSet];
|
|
276
|
+
if (!tier) {
|
|
277
|
+
tiers[level.codecSet] = tier = {
|
|
278
|
+
minBitrate: Infinity,
|
|
279
|
+
minHeight: Infinity,
|
|
280
|
+
minFramerate: Infinity,
|
|
281
|
+
minIndex: index,
|
|
282
|
+
maxScore: 0,
|
|
283
|
+
videoRanges: { SDR: 0 },
|
|
284
|
+
channels: { '2': 0 },
|
|
285
|
+
hasDefaultAudio: !audioGroups,
|
|
286
|
+
fragmentError: 0,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
tier.minBitrate = Math.min(tier.minBitrate, level.bitrate);
|
|
290
|
+
const lesserWidthOrHeight = Math.min(level.height, level.width);
|
|
291
|
+
tier.minHeight = Math.min(tier.minHeight, lesserWidthOrHeight);
|
|
292
|
+
tier.minFramerate = Math.min(tier.minFramerate, level.frameRate);
|
|
293
|
+
tier.minIndex = Math.min(tier.minIndex, index);
|
|
294
|
+
tier.maxScore = Math.max(tier.maxScore, level.score);
|
|
295
|
+
tier.fragmentError += level.fragmentError;
|
|
296
|
+
tier.videoRanges[level.videoRange] =
|
|
297
|
+
(tier.videoRanges[level.videoRange] || 0) + 1;
|
|
298
|
+
if (__USE_ALT_AUDIO__ && audioGroups) {
|
|
299
|
+
audioGroups.forEach((audioGroupId) => {
|
|
300
|
+
if (!audioGroupId) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const audioGroup = audioTracksByGroup.groups[audioGroupId];
|
|
304
|
+
if (!audioGroup) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Default audio is any group with DEFAULT=YES, or if missing then any group with AUTOSELECT=YES, or all variants
|
|
308
|
+
tier.hasDefaultAudio =
|
|
309
|
+
tier.hasDefaultAudio || audioTracksByGroup.hasDefaultAudio
|
|
310
|
+
? audioGroup.hasDefault
|
|
311
|
+
: audioGroup.hasAutoSelect ||
|
|
312
|
+
(!audioTracksByGroup.hasDefaultAudio &&
|
|
313
|
+
!audioTracksByGroup.hasAutoSelectAudio);
|
|
314
|
+
Object.keys(audioGroup.channels).forEach((channels) => {
|
|
315
|
+
tier.channels[channels] =
|
|
316
|
+
(tier.channels[channels] || 0) + audioGroup.channels[channels];
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return tiers;
|
|
321
|
+
}, {});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function getBasicSelectionOption(
|
|
325
|
+
option:
|
|
326
|
+
| MediaPlaylist
|
|
327
|
+
| AudioSelectionOption
|
|
328
|
+
| SubtitleSelectionOption
|
|
329
|
+
| undefined,
|
|
330
|
+
): Partial<AudioSelectionOption | SubtitleSelectionOption> | undefined {
|
|
331
|
+
if (!option) {
|
|
332
|
+
return option;
|
|
333
|
+
}
|
|
334
|
+
const { lang, assocLang, characteristics, channels, audioCodec } =
|
|
335
|
+
option as AudioSelectionOption;
|
|
336
|
+
return { lang, assocLang, characteristics, channels, audioCodec };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function findMatchingOption(
|
|
340
|
+
option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption,
|
|
341
|
+
tracks: MediaPlaylist[],
|
|
342
|
+
matchPredicate?: (
|
|
343
|
+
option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption,
|
|
344
|
+
track: MediaPlaylist,
|
|
345
|
+
) => boolean,
|
|
346
|
+
): number {
|
|
347
|
+
if ('attrs' in option) {
|
|
348
|
+
const index = tracks.indexOf(option);
|
|
349
|
+
if (index !== -1) {
|
|
350
|
+
return index;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
354
|
+
const track = tracks[i];
|
|
355
|
+
if (matchesOption(option, track, matchPredicate)) {
|
|
356
|
+
return i;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return -1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function matchesOption(
|
|
363
|
+
option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption,
|
|
364
|
+
track: MediaPlaylist,
|
|
365
|
+
matchPredicate?: (
|
|
366
|
+
option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption,
|
|
367
|
+
track: MediaPlaylist,
|
|
368
|
+
) => boolean,
|
|
369
|
+
): boolean {
|
|
370
|
+
const { groupId, name, lang, assocLang, default: isDefault } = option;
|
|
371
|
+
const forced = (option as SubtitleSelectionOption).forced;
|
|
372
|
+
return (
|
|
373
|
+
(groupId === undefined || track.groupId === groupId) &&
|
|
374
|
+
(name === undefined || track.name === name) &&
|
|
375
|
+
(lang === undefined || languagesMatch(lang, track.lang)) &&
|
|
376
|
+
(lang === undefined || track.assocLang === assocLang) &&
|
|
377
|
+
(isDefault === undefined || track.default === isDefault) &&
|
|
378
|
+
(forced === undefined || track.forced === forced) &&
|
|
379
|
+
(!('characteristics' in option) ||
|
|
380
|
+
characteristicsMatch(
|
|
381
|
+
option.characteristics || '',
|
|
382
|
+
track.characteristics,
|
|
383
|
+
)) &&
|
|
384
|
+
(matchPredicate === undefined || matchPredicate(option, track))
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function languagesMatch(languageA: string, languageB: string = '--'): boolean {
|
|
389
|
+
if (languageA.length === languageB.length) {
|
|
390
|
+
return languageA === languageB;
|
|
391
|
+
}
|
|
392
|
+
return languageA.startsWith(languageB) || languageB.startsWith(languageA);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function characteristicsMatch(
|
|
396
|
+
characteristicsA: string,
|
|
397
|
+
characteristicsB: string = '',
|
|
398
|
+
): boolean {
|
|
399
|
+
const arrA = characteristicsA.split(',');
|
|
400
|
+
const arrB = characteristicsB.split(',');
|
|
401
|
+
// Expects each item to be unique:
|
|
402
|
+
return (
|
|
403
|
+
arrA.length === arrB.length && !arrA.some((el) => arrB.indexOf(el) === -1)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function audioMatchPredicate(
|
|
408
|
+
option: MediaPlaylist | AudioSelectionOption,
|
|
409
|
+
track: MediaPlaylist,
|
|
410
|
+
) {
|
|
411
|
+
const { audioCodec, channels } = option;
|
|
412
|
+
return (
|
|
413
|
+
(audioCodec === undefined ||
|
|
414
|
+
(track.audioCodec || '').substring(0, 4) ===
|
|
415
|
+
audioCodec.substring(0, 4)) &&
|
|
416
|
+
(channels === undefined || channels === (track.channels || '2'))
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function findClosestLevelWithAudioGroup(
|
|
421
|
+
option: MediaPlaylist | AudioSelectionOption,
|
|
422
|
+
levels: Level[],
|
|
423
|
+
allAudioTracks: MediaPlaylist[],
|
|
424
|
+
searchIndex: number,
|
|
425
|
+
matchPredicate: (
|
|
426
|
+
option: MediaPlaylist | AudioSelectionOption,
|
|
427
|
+
track: MediaPlaylist,
|
|
428
|
+
) => boolean,
|
|
429
|
+
): number {
|
|
430
|
+
const currentLevel = levels[searchIndex];
|
|
431
|
+
// Are there variants with same URI as current level?
|
|
432
|
+
// If so, find a match that does not require any level URI change
|
|
433
|
+
const variants = levels.reduce(
|
|
434
|
+
(variantMap: { [uri: string]: number[] }, level, index) => {
|
|
435
|
+
const uri = level.uri;
|
|
436
|
+
const renditions = variantMap[uri] || (variantMap[uri] = []);
|
|
437
|
+
renditions.push(index);
|
|
438
|
+
return variantMap;
|
|
439
|
+
},
|
|
440
|
+
{},
|
|
441
|
+
);
|
|
442
|
+
const renditions = variants[currentLevel.uri];
|
|
443
|
+
if (renditions.length > 1) {
|
|
444
|
+
searchIndex = Math.max.apply(Math, renditions);
|
|
445
|
+
}
|
|
446
|
+
// Find best match
|
|
447
|
+
const currentVideoRange = currentLevel.videoRange;
|
|
448
|
+
const currentFrameRate = currentLevel.frameRate;
|
|
449
|
+
const currentVideoCodec = currentLevel.codecSet.substring(0, 4);
|
|
450
|
+
const matchingVideo = searchDownAndUpList(
|
|
451
|
+
levels,
|
|
452
|
+
searchIndex,
|
|
453
|
+
(level: Level) => {
|
|
454
|
+
if (
|
|
455
|
+
level.videoRange !== currentVideoRange ||
|
|
456
|
+
level.frameRate !== currentFrameRate ||
|
|
457
|
+
level.codecSet.substring(0, 4) !== currentVideoCodec
|
|
458
|
+
) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const audioGroups = level.audioGroups;
|
|
462
|
+
const tracks = allAudioTracks.filter(
|
|
463
|
+
(track): boolean =>
|
|
464
|
+
!audioGroups || audioGroups.indexOf(track.groupId) !== -1,
|
|
465
|
+
);
|
|
466
|
+
return findMatchingOption(option, tracks, matchPredicate) > -1;
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
if (matchingVideo > -1) {
|
|
470
|
+
return matchingVideo;
|
|
471
|
+
}
|
|
472
|
+
return searchDownAndUpList(levels, searchIndex, (level: Level) => {
|
|
473
|
+
const audioGroups = level.audioGroups;
|
|
474
|
+
const tracks = allAudioTracks.filter(
|
|
475
|
+
(track): boolean =>
|
|
476
|
+
!audioGroups || audioGroups.indexOf(track.groupId) !== -1,
|
|
477
|
+
);
|
|
478
|
+
return findMatchingOption(option, tracks, matchPredicate) > -1;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function searchDownAndUpList(
|
|
483
|
+
arr: any[],
|
|
484
|
+
searchIndex: number,
|
|
485
|
+
predicate: (item: any) => boolean,
|
|
486
|
+
): number {
|
|
487
|
+
for (let i = searchIndex; i > -1; i--) {
|
|
488
|
+
if (predicate(arr[i])) {
|
|
489
|
+
return i;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (let i = searchIndex + 1; i < arr.length; i++) {
|
|
493
|
+
if (predicate(arr[i])) {
|
|
494
|
+
return i;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return -1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function useAlternateAudio(
|
|
501
|
+
audioTrackUrl: string | undefined,
|
|
502
|
+
hls: Hls,
|
|
503
|
+
): boolean {
|
|
504
|
+
return !!audioTrackUrl && audioTrackUrl !== hls.loadLevelObj?.uri;
|
|
505
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const omitCircularRefsReplacer = (
|
|
2
|
+
replacer: ((this: any, key: string, value: any) => any) | undefined,
|
|
3
|
+
) => {
|
|
4
|
+
const known = new WeakSet();
|
|
5
|
+
return (_, value) => {
|
|
6
|
+
if (replacer) {
|
|
7
|
+
value = replacer(_, value);
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === 'object' && value !== null) {
|
|
10
|
+
if (known.has(value)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
known.add(value);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const stringify = <T>(
|
|
20
|
+
object: T,
|
|
21
|
+
replacer?: (this: any, key: string, value: any) => any,
|
|
22
|
+
): string => JSON.stringify(object, omitCircularRefsReplacer(replacer));
|