@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,1072 @@
|
|
|
1
|
+
import { buildAbsoluteURL } from 'url-toolkit';
|
|
2
|
+
import { DateRange } from './date-range';
|
|
3
|
+
import { Fragment, Part } from './fragment';
|
|
4
|
+
import { LevelDetails } from './level-details';
|
|
5
|
+
import { LevelKey } from './level-key';
|
|
6
|
+
import { PlaylistLevelType } from '../types/loader';
|
|
7
|
+
import { AttrList } from '../utils/attr-list';
|
|
8
|
+
import { isCodecType } from '../utils/codecs';
|
|
9
|
+
import { logger } from '../utils/logger';
|
|
10
|
+
import {
|
|
11
|
+
addVariableDefinition,
|
|
12
|
+
hasVariableReferences,
|
|
13
|
+
importVariableDefinition,
|
|
14
|
+
substituteVariables,
|
|
15
|
+
} from '../utils/variable-substitution';
|
|
16
|
+
import type { MediaFragment } from './fragment';
|
|
17
|
+
import type { ContentSteeringOptions } from '../types/events';
|
|
18
|
+
import type {
|
|
19
|
+
CodecsParsed,
|
|
20
|
+
LevelAttributes,
|
|
21
|
+
LevelParsed,
|
|
22
|
+
VariableMap,
|
|
23
|
+
} from '../types/level';
|
|
24
|
+
import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
|
|
25
|
+
import type { CodecType } from '../utils/codecs';
|
|
26
|
+
|
|
27
|
+
type M3U8ParserFragments = Array<Fragment | null>;
|
|
28
|
+
|
|
29
|
+
export type ParsedMultivariantPlaylist = {
|
|
30
|
+
contentSteering: ContentSteeringOptions | null;
|
|
31
|
+
levels: LevelParsed[];
|
|
32
|
+
playlistParsingError: Error | null;
|
|
33
|
+
sessionData: Record<string, AttrList> | null;
|
|
34
|
+
sessionKeys: LevelKey[] | null;
|
|
35
|
+
startTimeOffset: number | null;
|
|
36
|
+
variableList: VariableMap | null;
|
|
37
|
+
hasVariableRefs: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ParsedMultivariantMediaOptions = {
|
|
41
|
+
AUDIO?: MediaPlaylist[];
|
|
42
|
+
SUBTITLES?: MediaPlaylist[];
|
|
43
|
+
'CLOSED-CAPTIONS'?: MediaPlaylist[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type LevelKeys = { [key: string]: LevelKey | undefined };
|
|
47
|
+
|
|
48
|
+
const MASTER_PLAYLIST_REGEX =
|
|
49
|
+
/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g;
|
|
50
|
+
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
|
|
51
|
+
|
|
52
|
+
const IS_MEDIA_PLAYLIST = /^#EXT(?:INF|-X-TARGETDURATION):/m; // Handle empty Media Playlist (first EXTINF not signaled, but TARGETDURATION present)
|
|
53
|
+
|
|
54
|
+
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
|
|
55
|
+
[
|
|
56
|
+
/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
|
|
57
|
+
/(?!#) *(\S[^\r\n]*)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
|
|
58
|
+
/#.*/.source, // All other non-segment oriented tags will match with all groups empty
|
|
59
|
+
].join('|'),
|
|
60
|
+
'g',
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp(
|
|
64
|
+
[
|
|
65
|
+
/#EXT-X-(PROGRAM-DATE-TIME|BYTERANGE|DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/
|
|
66
|
+
.source,
|
|
67
|
+
/#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/
|
|
68
|
+
.source,
|
|
69
|
+
/#EXT-X-(DISCONTINUITY|ENDLIST|GAP|INDEPENDENT-SEGMENTS)/.source,
|
|
70
|
+
/(#)([^:]*):(.*)/.source,
|
|
71
|
+
/(#)(.*)(?:.*)\r?\n?/.source,
|
|
72
|
+
].join('|'),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export default class M3U8Parser {
|
|
76
|
+
static findGroup(
|
|
77
|
+
groups: (
|
|
78
|
+
| { id?: string; audioCodec?: string }
|
|
79
|
+
| { id?: string; textCodec?: string }
|
|
80
|
+
)[],
|
|
81
|
+
mediaGroupId: string,
|
|
82
|
+
):
|
|
83
|
+
| { id?: string; audioCodec?: string }
|
|
84
|
+
| { id?: string; textCodec?: string }
|
|
85
|
+
| undefined {
|
|
86
|
+
for (let i = 0; i < groups.length; i++) {
|
|
87
|
+
const group = groups[i];
|
|
88
|
+
if (group.id === mediaGroupId) {
|
|
89
|
+
return group;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static resolve(url, baseUrl) {
|
|
95
|
+
return buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static isMediaPlaylist(str: string): boolean {
|
|
99
|
+
return IS_MEDIA_PLAYLIST.test(str);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static parseMasterPlaylist(
|
|
103
|
+
string: string,
|
|
104
|
+
baseurl: string,
|
|
105
|
+
): ParsedMultivariantPlaylist {
|
|
106
|
+
const hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
|
|
107
|
+
? hasVariableReferences(string)
|
|
108
|
+
: false;
|
|
109
|
+
const parsed: ParsedMultivariantPlaylist = {
|
|
110
|
+
contentSteering: null,
|
|
111
|
+
levels: [],
|
|
112
|
+
playlistParsingError: null,
|
|
113
|
+
sessionData: null,
|
|
114
|
+
sessionKeys: null,
|
|
115
|
+
startTimeOffset: null,
|
|
116
|
+
variableList: null,
|
|
117
|
+
hasVariableRefs,
|
|
118
|
+
};
|
|
119
|
+
const levelsWithKnownCodecs: LevelParsed[] = [];
|
|
120
|
+
|
|
121
|
+
MASTER_PLAYLIST_REGEX.lastIndex = 0;
|
|
122
|
+
if (!string.startsWith('#EXTM3U')) {
|
|
123
|
+
parsed.playlistParsingError = new Error('no EXTM3U delimiter');
|
|
124
|
+
return parsed;
|
|
125
|
+
}
|
|
126
|
+
let result: RegExpExecArray | null;
|
|
127
|
+
while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
|
|
128
|
+
if (result[1]) {
|
|
129
|
+
// '#EXT-X-STREAM-INF' is found, parse level tag in group 1
|
|
130
|
+
const attrs = new AttrList(result[1], parsed) as LevelAttributes;
|
|
131
|
+
const uri = __USE_VARIABLE_SUBSTITUTION__
|
|
132
|
+
? substituteVariables(parsed, result[2])
|
|
133
|
+
: result[2];
|
|
134
|
+
const level: LevelParsed = {
|
|
135
|
+
attrs,
|
|
136
|
+
bitrate:
|
|
137
|
+
attrs.decimalInteger('BANDWIDTH') ||
|
|
138
|
+
attrs.decimalInteger('AVERAGE-BANDWIDTH'),
|
|
139
|
+
name: attrs.NAME,
|
|
140
|
+
url: M3U8Parser.resolve(uri, baseurl),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const resolution = attrs.decimalResolution('RESOLUTION');
|
|
144
|
+
if (resolution) {
|
|
145
|
+
level.width = resolution.width;
|
|
146
|
+
level.height = resolution.height;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setCodecs(attrs.CODECS, level);
|
|
150
|
+
const supplementalCodecs = attrs['SUPPLEMENTAL-CODECS'];
|
|
151
|
+
if (supplementalCodecs) {
|
|
152
|
+
level.supplemental = {};
|
|
153
|
+
setCodecs(supplementalCodecs, level.supplemental);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!level.unknownCodecs?.length) {
|
|
157
|
+
levelsWithKnownCodecs.push(level);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parsed.levels.push(level);
|
|
161
|
+
} else if (result[3]) {
|
|
162
|
+
const tag = result[3];
|
|
163
|
+
const attributes = result[4];
|
|
164
|
+
switch (tag) {
|
|
165
|
+
case 'SESSION-DATA': {
|
|
166
|
+
// #EXT-X-SESSION-DATA
|
|
167
|
+
const sessionAttrs = new AttrList(attributes, parsed);
|
|
168
|
+
const dataId = sessionAttrs['DATA-ID'];
|
|
169
|
+
if (dataId) {
|
|
170
|
+
if (parsed.sessionData === null) {
|
|
171
|
+
parsed.sessionData = {};
|
|
172
|
+
}
|
|
173
|
+
parsed.sessionData[dataId] = sessionAttrs;
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case 'SESSION-KEY': {
|
|
178
|
+
// #EXT-X-SESSION-KEY
|
|
179
|
+
const sessionKey = parseKey(attributes, baseurl, parsed);
|
|
180
|
+
if (sessionKey.encrypted && sessionKey.isSupported()) {
|
|
181
|
+
if (parsed.sessionKeys === null) {
|
|
182
|
+
parsed.sessionKeys = [];
|
|
183
|
+
}
|
|
184
|
+
parsed.sessionKeys.push(sessionKey);
|
|
185
|
+
} else {
|
|
186
|
+
logger.warn(
|
|
187
|
+
`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${attributes}"`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'DEFINE': {
|
|
193
|
+
// #EXT-X-DEFINE
|
|
194
|
+
if (__USE_VARIABLE_SUBSTITUTION__) {
|
|
195
|
+
const variableAttributes = new AttrList(attributes, parsed);
|
|
196
|
+
addVariableDefinition(parsed, variableAttributes, baseurl);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'CONTENT-STEERING': {
|
|
201
|
+
// #EXT-X-CONTENT-STEERING
|
|
202
|
+
const contentSteeringAttributes = new AttrList(attributes, parsed);
|
|
203
|
+
parsed.contentSteering = {
|
|
204
|
+
uri: M3U8Parser.resolve(
|
|
205
|
+
contentSteeringAttributes['SERVER-URI'],
|
|
206
|
+
baseurl,
|
|
207
|
+
),
|
|
208
|
+
pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.',
|
|
209
|
+
};
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 'START': {
|
|
213
|
+
// #EXT-X-START
|
|
214
|
+
parsed.startTimeOffset = parseStartTimeOffset(attributes);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Filter out levels with unknown codecs if it does not remove all levels
|
|
223
|
+
const stripUnknownCodecLevels =
|
|
224
|
+
levelsWithKnownCodecs.length > 0 &&
|
|
225
|
+
levelsWithKnownCodecs.length < parsed.levels.length;
|
|
226
|
+
|
|
227
|
+
parsed.levels = stripUnknownCodecLevels
|
|
228
|
+
? levelsWithKnownCodecs
|
|
229
|
+
: parsed.levels;
|
|
230
|
+
if (parsed.levels.length === 0) {
|
|
231
|
+
parsed.playlistParsingError = new Error('no levels found in manifest');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return parsed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static parseMasterPlaylistMedia(
|
|
238
|
+
string: string,
|
|
239
|
+
baseurl: string,
|
|
240
|
+
parsed: ParsedMultivariantPlaylist,
|
|
241
|
+
): ParsedMultivariantMediaOptions {
|
|
242
|
+
let result: RegExpExecArray | null;
|
|
243
|
+
const results: ParsedMultivariantMediaOptions = {};
|
|
244
|
+
const levels = parsed.levels;
|
|
245
|
+
const groupsByType = {
|
|
246
|
+
AUDIO: levels.map((level: LevelParsed) => ({
|
|
247
|
+
id: level.attrs.AUDIO,
|
|
248
|
+
audioCodec: level.audioCodec,
|
|
249
|
+
})),
|
|
250
|
+
SUBTITLES: levels.map((level: LevelParsed) => ({
|
|
251
|
+
id: level.attrs.SUBTITLES,
|
|
252
|
+
textCodec: level.textCodec,
|
|
253
|
+
})),
|
|
254
|
+
'CLOSED-CAPTIONS': [],
|
|
255
|
+
};
|
|
256
|
+
let id = 0;
|
|
257
|
+
MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
|
|
258
|
+
while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
|
|
259
|
+
const attrs = new AttrList(result[1], parsed) as MediaAttributes;
|
|
260
|
+
const type = attrs.TYPE;
|
|
261
|
+
if (type) {
|
|
262
|
+
const groups:
|
|
263
|
+
| (typeof groupsByType)[keyof typeof groupsByType]
|
|
264
|
+
| undefined = groupsByType[type];
|
|
265
|
+
const medias: MediaPlaylist[] = results[type] || [];
|
|
266
|
+
results[type] = medias;
|
|
267
|
+
const lang = attrs.LANGUAGE;
|
|
268
|
+
const assocLang = attrs['ASSOC-LANGUAGE'];
|
|
269
|
+
const channels = attrs.CHANNELS;
|
|
270
|
+
const characteristics = attrs.CHARACTERISTICS;
|
|
271
|
+
const instreamId = attrs['INSTREAM-ID'];
|
|
272
|
+
const media: MediaPlaylist = {
|
|
273
|
+
attrs,
|
|
274
|
+
bitrate: 0,
|
|
275
|
+
id: id++,
|
|
276
|
+
groupId: attrs['GROUP-ID'] || '',
|
|
277
|
+
name: attrs.NAME || lang || '',
|
|
278
|
+
type,
|
|
279
|
+
default: attrs.bool('DEFAULT'),
|
|
280
|
+
autoselect: attrs.bool('AUTOSELECT'),
|
|
281
|
+
forced: attrs.bool('FORCED'),
|
|
282
|
+
lang,
|
|
283
|
+
url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '',
|
|
284
|
+
};
|
|
285
|
+
if (assocLang) {
|
|
286
|
+
media.assocLang = assocLang;
|
|
287
|
+
}
|
|
288
|
+
if (channels) {
|
|
289
|
+
media.channels = channels;
|
|
290
|
+
}
|
|
291
|
+
if (characteristics) {
|
|
292
|
+
media.characteristics = characteristics;
|
|
293
|
+
}
|
|
294
|
+
if (instreamId) {
|
|
295
|
+
media.instreamId = instreamId;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (groups?.length) {
|
|
299
|
+
// If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track
|
|
300
|
+
// If we don't find the track signalled, lets use the first audio groups codec we have
|
|
301
|
+
// Acting as a best guess
|
|
302
|
+
const groupCodec =
|
|
303
|
+
M3U8Parser.findGroup(groups, media.groupId as string) || groups[0];
|
|
304
|
+
assignCodec(media, groupCodec, 'audioCodec');
|
|
305
|
+
assignCodec(media, groupCodec, 'textCodec');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
medias.push(media);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
static parseLevelPlaylist(
|
|
315
|
+
string: string,
|
|
316
|
+
baseurl: string,
|
|
317
|
+
id: number,
|
|
318
|
+
type: PlaylistLevelType,
|
|
319
|
+
levelUrlId: number,
|
|
320
|
+
multivariantVariableList: VariableMap | null,
|
|
321
|
+
algoSegmentPattern?: RegExp | string | null,
|
|
322
|
+
): LevelDetails {
|
|
323
|
+
const base = { url: baseurl };
|
|
324
|
+
const level = new LevelDetails(baseurl);
|
|
325
|
+
const fragments: M3U8ParserFragments = level.fragments;
|
|
326
|
+
const programDateTimes: MediaFragment[] = [];
|
|
327
|
+
// The most recent init segment seen (applies to all subsequent segments)
|
|
328
|
+
let currentInitSegment: Fragment | null = null;
|
|
329
|
+
let currentSN = 0;
|
|
330
|
+
let currentPart = 0;
|
|
331
|
+
let totalduration = 0;
|
|
332
|
+
let discontinuityCounter = 0;
|
|
333
|
+
let currentBitrate = 0;
|
|
334
|
+
let prevFrag: Fragment | null = null;
|
|
335
|
+
let frag: Fragment = new Fragment(type, base);
|
|
336
|
+
let result: RegExpExecArray | RegExpMatchArray | null;
|
|
337
|
+
let i: number;
|
|
338
|
+
let levelkeys: LevelKeys | undefined;
|
|
339
|
+
let firstPdtIndex = -1;
|
|
340
|
+
let createNextFrag = false;
|
|
341
|
+
let nextByteRange: string | null = null;
|
|
342
|
+
let serverControlAttrs: AttrList | undefined;
|
|
343
|
+
|
|
344
|
+
LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0;
|
|
345
|
+
level.m3u8 = string;
|
|
346
|
+
level.hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
|
|
347
|
+
? hasVariableReferences(string)
|
|
348
|
+
: false;
|
|
349
|
+
if (LEVEL_PLAYLIST_REGEX_FAST.exec(string)?.[0] !== '#EXTM3U') {
|
|
350
|
+
level.playlistParsingError = new Error(
|
|
351
|
+
'Missing format identifier #EXTM3U',
|
|
352
|
+
);
|
|
353
|
+
return level;
|
|
354
|
+
}
|
|
355
|
+
while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
|
|
356
|
+
if (createNextFrag) {
|
|
357
|
+
createNextFrag = false;
|
|
358
|
+
frag = new Fragment(type, base);
|
|
359
|
+
// setup the next fragment for part loading
|
|
360
|
+
frag.playlistOffset = totalduration;
|
|
361
|
+
frag.setStart(totalduration);
|
|
362
|
+
frag.sn = currentSN;
|
|
363
|
+
frag.cc = discontinuityCounter;
|
|
364
|
+
if (currentBitrate) {
|
|
365
|
+
frag.bitrate = currentBitrate;
|
|
366
|
+
}
|
|
367
|
+
frag.level = id;
|
|
368
|
+
if (currentInitSegment) {
|
|
369
|
+
frag.initSegment = currentInitSegment;
|
|
370
|
+
if (currentInitSegment.rawProgramDateTime) {
|
|
371
|
+
frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
|
|
372
|
+
currentInitSegment.rawProgramDateTime = null;
|
|
373
|
+
}
|
|
374
|
+
if (nextByteRange) {
|
|
375
|
+
frag.setByteRange(nextByteRange);
|
|
376
|
+
nextByteRange = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const duration = result[1];
|
|
382
|
+
if (duration) {
|
|
383
|
+
// INF
|
|
384
|
+
frag.duration = parseFloat(duration);
|
|
385
|
+
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
|
386
|
+
const title = (' ' + result[2]).slice(1);
|
|
387
|
+
frag.title = title || null;
|
|
388
|
+
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
|
|
389
|
+
} else if (result[3]) {
|
|
390
|
+
// url
|
|
391
|
+
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
|
392
|
+
const uri = (' ' + result[3]).slice(1);
|
|
393
|
+
const relurl = __USE_VARIABLE_SUBSTITUTION__
|
|
394
|
+
? substituteVariables(level, uri)
|
|
395
|
+
: uri;
|
|
396
|
+
const shouldTreatAsAlgo =
|
|
397
|
+
type === PlaylistLevelType.MAIN &&
|
|
398
|
+
!!algoSegmentPattern &&
|
|
399
|
+
(isAlgoSegment(relurl, algoSegmentPattern) ||
|
|
400
|
+
isLikelyAlgoSegment(relurl, frag.duration));
|
|
401
|
+
if (shouldTreatAsAlgo) {
|
|
402
|
+
if (prevFrag) {
|
|
403
|
+
prevFrag.algoRelurl = relurl;
|
|
404
|
+
} else {
|
|
405
|
+
logger.warn(
|
|
406
|
+
`[m3u8-parser] 发现算法分片但缺少前序视频分片:${relurl}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (Number.isFinite(frag.duration)) {
|
|
412
|
+
frag.playlistOffset = totalduration;
|
|
413
|
+
frag.setStart(totalduration);
|
|
414
|
+
if (levelkeys) {
|
|
415
|
+
setFragLevelKeys(frag, levelkeys, level);
|
|
416
|
+
}
|
|
417
|
+
frag.sn = currentSN;
|
|
418
|
+
frag.level = id;
|
|
419
|
+
frag.cc = discontinuityCounter;
|
|
420
|
+
fragments.push(frag);
|
|
421
|
+
frag.relurl = relurl;
|
|
422
|
+
assignProgramDateTime(
|
|
423
|
+
frag as MediaFragment,
|
|
424
|
+
prevFrag as MediaFragment,
|
|
425
|
+
programDateTimes,
|
|
426
|
+
);
|
|
427
|
+
prevFrag = frag;
|
|
428
|
+
totalduration += frag.duration;
|
|
429
|
+
currentSN++;
|
|
430
|
+
currentPart = 0;
|
|
431
|
+
createNextFrag = true;
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW);
|
|
435
|
+
if (!result) {
|
|
436
|
+
logger.warn('No matches on slow regex match for level playlist!');
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
for (i = 1; i < result.length; i++) {
|
|
440
|
+
if ((result[i] as any) !== undefined) {
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
|
446
|
+
const tag = (' ' + result[i]).slice(1);
|
|
447
|
+
const value1 = (' ' + result[i + 1]).slice(1);
|
|
448
|
+
const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : null;
|
|
449
|
+
|
|
450
|
+
switch (tag) {
|
|
451
|
+
case 'BYTERANGE':
|
|
452
|
+
if (prevFrag) {
|
|
453
|
+
frag.setByteRange(value1, prevFrag);
|
|
454
|
+
} else {
|
|
455
|
+
frag.setByteRange(value1);
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
case 'PROGRAM-DATE-TIME':
|
|
459
|
+
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
|
460
|
+
frag.rawProgramDateTime = value1;
|
|
461
|
+
frag.tagList.push(['PROGRAM-DATE-TIME', value1]);
|
|
462
|
+
if (firstPdtIndex === -1) {
|
|
463
|
+
firstPdtIndex = fragments.length;
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
case 'PLAYLIST-TYPE':
|
|
467
|
+
if (level.type) {
|
|
468
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
469
|
+
}
|
|
470
|
+
level.type = value1.toUpperCase();
|
|
471
|
+
break;
|
|
472
|
+
case 'MEDIA-SEQUENCE':
|
|
473
|
+
if (level.startSN !== 0) {
|
|
474
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
475
|
+
} else if (fragments.length > 0) {
|
|
476
|
+
assignMustAppearBeforeSegmentsError(level, tag, result);
|
|
477
|
+
}
|
|
478
|
+
currentSN = level.startSN = parseInt(value1);
|
|
479
|
+
break;
|
|
480
|
+
case 'SKIP': {
|
|
481
|
+
if (level.skippedSegments) {
|
|
482
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
483
|
+
}
|
|
484
|
+
const skipAttrs = new AttrList(value1, level);
|
|
485
|
+
const skippedSegments =
|
|
486
|
+
skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
|
|
487
|
+
if (Number.isFinite(skippedSegments)) {
|
|
488
|
+
level.skippedSegments += skippedSegments;
|
|
489
|
+
// This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
|
|
490
|
+
for (let i = skippedSegments; i--; ) {
|
|
491
|
+
fragments.push(null);
|
|
492
|
+
}
|
|
493
|
+
currentSN += skippedSegments;
|
|
494
|
+
}
|
|
495
|
+
const recentlyRemovedDateranges = skipAttrs.enumeratedString(
|
|
496
|
+
'RECENTLY-REMOVED-DATERANGES',
|
|
497
|
+
);
|
|
498
|
+
if (recentlyRemovedDateranges) {
|
|
499
|
+
level.recentlyRemovedDateranges = (
|
|
500
|
+
level.recentlyRemovedDateranges || []
|
|
501
|
+
).concat(recentlyRemovedDateranges.split('\t'));
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case 'TARGETDURATION':
|
|
506
|
+
if (level.targetduration !== 0) {
|
|
507
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
508
|
+
}
|
|
509
|
+
level.targetduration = Math.max(parseInt(value1), 1);
|
|
510
|
+
break;
|
|
511
|
+
case 'VERSION':
|
|
512
|
+
if (level.version !== null) {
|
|
513
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
514
|
+
}
|
|
515
|
+
level.version = parseInt(value1);
|
|
516
|
+
break;
|
|
517
|
+
case 'INDEPENDENT-SEGMENTS':
|
|
518
|
+
break;
|
|
519
|
+
case 'ENDLIST':
|
|
520
|
+
if (!level.live) {
|
|
521
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
522
|
+
}
|
|
523
|
+
level.live = false;
|
|
524
|
+
break;
|
|
525
|
+
case '#':
|
|
526
|
+
if (value1 || value2) {
|
|
527
|
+
frag.tagList.push(value2 ? [value1, value2] : [value1]);
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
case 'DISCONTINUITY':
|
|
531
|
+
discontinuityCounter++;
|
|
532
|
+
frag.tagList.push(['DIS']);
|
|
533
|
+
break;
|
|
534
|
+
case 'GAP':
|
|
535
|
+
frag.gap = true;
|
|
536
|
+
frag.tagList.push([tag]);
|
|
537
|
+
break;
|
|
538
|
+
case 'BITRATE':
|
|
539
|
+
frag.tagList.push([tag, value1]);
|
|
540
|
+
currentBitrate = parseInt(value1) * 1000;
|
|
541
|
+
if (Number.isFinite(currentBitrate)) {
|
|
542
|
+
frag.bitrate = currentBitrate;
|
|
543
|
+
} else {
|
|
544
|
+
currentBitrate = 0;
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
case 'DATERANGE': {
|
|
548
|
+
const dateRangeAttr = new AttrList(value1, level);
|
|
549
|
+
const dateRange = new DateRange(
|
|
550
|
+
dateRangeAttr,
|
|
551
|
+
level.dateRanges[dateRangeAttr.ID],
|
|
552
|
+
level.dateRangeTagCount,
|
|
553
|
+
);
|
|
554
|
+
level.dateRangeTagCount++;
|
|
555
|
+
if (dateRange.isValid || level.skippedSegments) {
|
|
556
|
+
level.dateRanges[dateRange.id] = dateRange;
|
|
557
|
+
} else {
|
|
558
|
+
logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`);
|
|
559
|
+
}
|
|
560
|
+
// Add to fragment tag list for backwards compatibility (< v1.2.0)
|
|
561
|
+
frag.tagList.push(['EXT-X-DATERANGE', value1]);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case 'DEFINE': {
|
|
565
|
+
if (__USE_VARIABLE_SUBSTITUTION__) {
|
|
566
|
+
const variableAttributes = new AttrList(value1, level);
|
|
567
|
+
if ('IMPORT' in variableAttributes) {
|
|
568
|
+
importVariableDefinition(
|
|
569
|
+
level,
|
|
570
|
+
variableAttributes,
|
|
571
|
+
multivariantVariableList,
|
|
572
|
+
);
|
|
573
|
+
} else {
|
|
574
|
+
addVariableDefinition(level, variableAttributes, baseurl);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
case 'DISCONTINUITY-SEQUENCE':
|
|
581
|
+
if (level.startCC !== 0) {
|
|
582
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
583
|
+
} else if (fragments.length > 0) {
|
|
584
|
+
assignMustAppearBeforeSegmentsError(level, tag, result);
|
|
585
|
+
}
|
|
586
|
+
level.startCC = discontinuityCounter = parseInt(value1);
|
|
587
|
+
break;
|
|
588
|
+
case 'KEY': {
|
|
589
|
+
const levelKey = parseKey(value1, baseurl, level);
|
|
590
|
+
if (levelKey.isSupported()) {
|
|
591
|
+
if (levelKey.method === 'NONE') {
|
|
592
|
+
levelkeys = undefined;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
if (!levelkeys) {
|
|
596
|
+
levelkeys = {};
|
|
597
|
+
}
|
|
598
|
+
const currentKey = levelkeys[levelKey.keyFormat];
|
|
599
|
+
// Ignore duplicate playlist KEY tags
|
|
600
|
+
if (!currentKey?.matches(levelKey)) {
|
|
601
|
+
if (currentKey) {
|
|
602
|
+
levelkeys = Object.assign({}, levelkeys);
|
|
603
|
+
}
|
|
604
|
+
levelkeys[levelKey.keyFormat] = levelKey;
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
logger.warn(
|
|
608
|
+
`[Keys] Ignoring unsupported EXT-X-KEY tag: "${value1}"${__USE_EME_DRM__ ? '' : ' (light build)'}`,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case 'START':
|
|
614
|
+
level.startTimeOffset = parseStartTimeOffset(value1);
|
|
615
|
+
break;
|
|
616
|
+
case 'MAP': {
|
|
617
|
+
const mapAttrs = new AttrList(value1, level);
|
|
618
|
+
if (frag.duration) {
|
|
619
|
+
// Initial segment tag is after segment duration tag.
|
|
620
|
+
// #EXTINF: 6.0
|
|
621
|
+
// #EXT-X-MAP:URI="init.mp4
|
|
622
|
+
const init = new Fragment(type, base);
|
|
623
|
+
setInitSegment(init, mapAttrs, id, levelkeys);
|
|
624
|
+
currentInitSegment = init;
|
|
625
|
+
frag.initSegment = currentInitSegment;
|
|
626
|
+
if (
|
|
627
|
+
currentInitSegment.rawProgramDateTime &&
|
|
628
|
+
!frag.rawProgramDateTime
|
|
629
|
+
) {
|
|
630
|
+
frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
// Initial segment tag is before segment duration tag
|
|
634
|
+
// Handle case where EXT-X-MAP is declared after EXT-X-BYTERANGE
|
|
635
|
+
const end = frag.byteRangeEndOffset;
|
|
636
|
+
if (end) {
|
|
637
|
+
const start = frag.byteRangeStartOffset as number;
|
|
638
|
+
nextByteRange = `${end - start}@${start}`;
|
|
639
|
+
} else {
|
|
640
|
+
nextByteRange = null;
|
|
641
|
+
}
|
|
642
|
+
setInitSegment(frag, mapAttrs, id, levelkeys);
|
|
643
|
+
currentInitSegment = frag;
|
|
644
|
+
createNextFrag = true;
|
|
645
|
+
}
|
|
646
|
+
currentInitSegment.cc = discontinuityCounter;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
case 'SERVER-CONTROL': {
|
|
650
|
+
if (serverControlAttrs) {
|
|
651
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
652
|
+
}
|
|
653
|
+
serverControlAttrs = new AttrList(value1);
|
|
654
|
+
level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD');
|
|
655
|
+
level.canSkipUntil = serverControlAttrs.optionalFloat(
|
|
656
|
+
'CAN-SKIP-UNTIL',
|
|
657
|
+
0,
|
|
658
|
+
);
|
|
659
|
+
level.canSkipDateRanges =
|
|
660
|
+
level.canSkipUntil > 0 &&
|
|
661
|
+
serverControlAttrs.bool('CAN-SKIP-DATERANGES');
|
|
662
|
+
level.partHoldBack = serverControlAttrs.optionalFloat(
|
|
663
|
+
'PART-HOLD-BACK',
|
|
664
|
+
0,
|
|
665
|
+
);
|
|
666
|
+
level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0);
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case 'PART-INF': {
|
|
670
|
+
if (level.partTarget) {
|
|
671
|
+
assignMultipleMediaPlaylistTagOccuranceError(level, tag, result);
|
|
672
|
+
}
|
|
673
|
+
const partInfAttrs = new AttrList(value1);
|
|
674
|
+
level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET');
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case 'PART': {
|
|
678
|
+
let partList = level.partList;
|
|
679
|
+
if (!partList) {
|
|
680
|
+
partList = level.partList = [];
|
|
681
|
+
}
|
|
682
|
+
const previousPart =
|
|
683
|
+
currentPart > 0 ? partList[partList.length - 1] : undefined;
|
|
684
|
+
const index = currentPart++;
|
|
685
|
+
const partAttrs = new AttrList(value1, level);
|
|
686
|
+
const part = new Part(
|
|
687
|
+
partAttrs,
|
|
688
|
+
frag as MediaFragment,
|
|
689
|
+
base,
|
|
690
|
+
index,
|
|
691
|
+
previousPart,
|
|
692
|
+
);
|
|
693
|
+
partList.push(part);
|
|
694
|
+
frag.duration += part.duration;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case 'PRELOAD-HINT': {
|
|
698
|
+
const preloadHintAttrs = new AttrList(value1, level);
|
|
699
|
+
level.preloadHint = preloadHintAttrs;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case 'RENDITION-REPORT': {
|
|
703
|
+
const renditionReportAttrs = new AttrList(value1, level);
|
|
704
|
+
level.renditionReports = level.renditionReports || [];
|
|
705
|
+
level.renditionReports.push(renditionReportAttrs);
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
default:
|
|
709
|
+
logger.warn(`line parsed but not handled: ${result}`);
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (prevFrag && !prevFrag.relurl) {
|
|
715
|
+
fragments.pop();
|
|
716
|
+
totalduration -= prevFrag.duration;
|
|
717
|
+
if (level.partList) {
|
|
718
|
+
level.fragmentHint = prevFrag as MediaFragment;
|
|
719
|
+
}
|
|
720
|
+
} else if (level.partList) {
|
|
721
|
+
assignProgramDateTime(
|
|
722
|
+
frag as MediaFragment,
|
|
723
|
+
prevFrag as MediaFragment,
|
|
724
|
+
programDateTimes,
|
|
725
|
+
);
|
|
726
|
+
frag.cc = discontinuityCounter;
|
|
727
|
+
level.fragmentHint = frag as MediaFragment;
|
|
728
|
+
if (levelkeys) {
|
|
729
|
+
setFragLevelKeys(frag, levelkeys, level);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (!level.targetduration) {
|
|
733
|
+
level.playlistParsingError = new Error(`Missing Target Duration`);
|
|
734
|
+
}
|
|
735
|
+
const fragmentLength = fragments.length;
|
|
736
|
+
const firstFragment = fragments[0];
|
|
737
|
+
const lastFragment = fragments[fragmentLength - 1];
|
|
738
|
+
totalduration += level.skippedSegments * level.targetduration;
|
|
739
|
+
if (totalduration > 0 && fragmentLength && lastFragment) {
|
|
740
|
+
level.averagetargetduration = totalduration / fragmentLength;
|
|
741
|
+
const lastSn = lastFragment.sn;
|
|
742
|
+
level.endSN = lastSn !== 'initSegment' ? lastSn : 0;
|
|
743
|
+
if (!level.live) {
|
|
744
|
+
lastFragment.endList = true;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Backfill any missing PDT values
|
|
748
|
+
* "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
|
|
749
|
+
* one or more Media Segment URIs, the client SHOULD extrapolate
|
|
750
|
+
* backward from that tag (using EXTINF durations and/or media
|
|
751
|
+
* timestamps) to associate dates with those segments."
|
|
752
|
+
* We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
|
|
753
|
+
* computed.
|
|
754
|
+
*/
|
|
755
|
+
if (firstPdtIndex > 0) {
|
|
756
|
+
backfillProgramDateTimes(fragments, firstPdtIndex);
|
|
757
|
+
if (firstFragment) {
|
|
758
|
+
programDateTimes.unshift(firstFragment as MediaFragment);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (level.fragmentHint) {
|
|
763
|
+
totalduration += level.fragmentHint.duration;
|
|
764
|
+
}
|
|
765
|
+
level.totalduration = totalduration;
|
|
766
|
+
if (programDateTimes.length && level.dateRangeTagCount && firstFragment) {
|
|
767
|
+
mapDateRanges(programDateTimes, level);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
level.endCC = discontinuityCounter;
|
|
771
|
+
|
|
772
|
+
return level;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function mapDateRanges(
|
|
777
|
+
programDateTimes: MediaFragment[],
|
|
778
|
+
details: LevelDetails,
|
|
779
|
+
) {
|
|
780
|
+
// Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
|
|
781
|
+
let programDateTimeCount = programDateTimes.length;
|
|
782
|
+
if (!programDateTimeCount) {
|
|
783
|
+
if (details.hasProgramDateTime) {
|
|
784
|
+
const lastFragment = details.fragments[details.fragments.length - 1];
|
|
785
|
+
programDateTimes.push(lastFragment);
|
|
786
|
+
programDateTimeCount++;
|
|
787
|
+
} else {
|
|
788
|
+
// no segments with EXT-X-PROGRAM-DATE-TIME references in playlist history
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
|
|
793
|
+
const playlistEnd = details.live ? Infinity : details.totalduration;
|
|
794
|
+
const dateRangeIds = Object.keys(details.dateRanges);
|
|
795
|
+
for (let i = dateRangeIds.length; i--; ) {
|
|
796
|
+
const dateRange = details.dateRanges[dateRangeIds[i]]!;
|
|
797
|
+
const startDateTime = dateRange.startDate.getTime();
|
|
798
|
+
dateRange.tagAnchor = lastProgramDateTime.ref;
|
|
799
|
+
for (let j = programDateTimeCount; j--; ) {
|
|
800
|
+
if (programDateTimes[j]?.sn < details.startSN) {
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
const fragIndex = findFragmentWithStartDate(
|
|
804
|
+
details,
|
|
805
|
+
startDateTime,
|
|
806
|
+
programDateTimes,
|
|
807
|
+
j,
|
|
808
|
+
playlistEnd,
|
|
809
|
+
);
|
|
810
|
+
if (fragIndex !== -1) {
|
|
811
|
+
dateRange.tagAnchor = details.fragments[fragIndex].ref;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const algoSegmentPatternWarnedMessages = new Set<string>();
|
|
819
|
+
|
|
820
|
+
function warnAlgoSegmentPattern(message: string): void {
|
|
821
|
+
if (algoSegmentPatternWarnedMessages.has(message)) return;
|
|
822
|
+
algoSegmentPatternWarnedMessages.add(message);
|
|
823
|
+
logger.warn(`[m3u8-parser] ${message}`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function isAlgoSegment(
|
|
827
|
+
uri: string,
|
|
828
|
+
algoSegmentPattern?: RegExp | string | null,
|
|
829
|
+
): boolean {
|
|
830
|
+
if (!algoSegmentPattern) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
// 先去掉查询参数和 hash,避免正则因结尾匹配失败
|
|
834
|
+
const cleanUri = uri.split(/[?#]/)[0];
|
|
835
|
+
if (algoSegmentPattern instanceof RegExp) {
|
|
836
|
+
algoSegmentPattern.lastIndex = 0;
|
|
837
|
+
return algoSegmentPattern.test(cleanUri);
|
|
838
|
+
}
|
|
839
|
+
if (typeof algoSegmentPattern !== 'string') {
|
|
840
|
+
warnAlgoSegmentPattern(
|
|
841
|
+
`algoSegmentPattern 类型非法,已忽略:${String(algoSegmentPattern)}`,
|
|
842
|
+
);
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
const trimmedPattern = algoSegmentPattern.trim();
|
|
846
|
+
if (!trimmedPattern) {
|
|
847
|
+
warnAlgoSegmentPattern('algoSegmentPattern 为空,已忽略');
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
return new RegExp(trimmedPattern).test(cleanUri);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
warnAlgoSegmentPattern(
|
|
854
|
+
`algoSegmentPattern 无效,已忽略:${algoSegmentPattern}`,
|
|
855
|
+
);
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function isLikelyAlgoSegment(uri: string, duration: number | null): boolean {
|
|
861
|
+
if (duration === null || !Number.isFinite(duration)) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
// 超短分片且后缀为 _dat.ts 时,视为算法分片兜底处理
|
|
865
|
+
if (duration > 0.02) {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
const cleanUri = uri.split(/[?#]/)[0];
|
|
869
|
+
return /_dat\.ts$/i.test(cleanUri);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function findFragmentWithStartDate(
|
|
873
|
+
details: LevelDetails,
|
|
874
|
+
startDateTime: number,
|
|
875
|
+
programDateTimes: MediaFragment[],
|
|
876
|
+
index: number,
|
|
877
|
+
endTime: number,
|
|
878
|
+
): number {
|
|
879
|
+
const pdtFragment = programDateTimes[index] as MediaFragment | undefined;
|
|
880
|
+
if (pdtFragment) {
|
|
881
|
+
// find matching range between PDT tags
|
|
882
|
+
const pdtStart = pdtFragment.programDateTime as number;
|
|
883
|
+
if (startDateTime >= pdtStart || index === 0) {
|
|
884
|
+
const durationBetweenPdt =
|
|
885
|
+
(programDateTimes[index + 1]?.start || endTime) - pdtFragment.start;
|
|
886
|
+
if (startDateTime <= pdtStart + durationBetweenPdt * 1000) {
|
|
887
|
+
// map to fragment with date-time range
|
|
888
|
+
const startIndex = programDateTimes[index].sn - details.startSN;
|
|
889
|
+
if (startIndex < 0) {
|
|
890
|
+
return -1;
|
|
891
|
+
}
|
|
892
|
+
const fragments = details.fragments;
|
|
893
|
+
if (fragments.length > programDateTimes.length) {
|
|
894
|
+
const endSegment =
|
|
895
|
+
programDateTimes[index + 1] || fragments[fragments.length - 1];
|
|
896
|
+
const endIndex = endSegment.sn - details.startSN;
|
|
897
|
+
for (let i = endIndex; i > startIndex; i--) {
|
|
898
|
+
const fragStartDateTime = fragments[i].programDateTime as number;
|
|
899
|
+
if (
|
|
900
|
+
startDateTime >= fragStartDateTime &&
|
|
901
|
+
startDateTime < fragStartDateTime + fragments[i].duration * 1000
|
|
902
|
+
) {
|
|
903
|
+
return i;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return startIndex;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return -1;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function parseKey(
|
|
915
|
+
keyTagAttributes: string,
|
|
916
|
+
baseurl: string,
|
|
917
|
+
parsed: ParsedMultivariantPlaylist | LevelDetails,
|
|
918
|
+
): LevelKey {
|
|
919
|
+
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
|
920
|
+
const keyAttrs = new AttrList(keyTagAttributes, parsed);
|
|
921
|
+
const decryptmethod = keyAttrs.METHOD ?? '';
|
|
922
|
+
const decrypturi = keyAttrs.URI;
|
|
923
|
+
const decryptiv = keyAttrs.hexadecimalInteger('IV');
|
|
924
|
+
const decryptkeyformatversions = keyAttrs.KEYFORMATVERSIONS;
|
|
925
|
+
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
|
|
926
|
+
const decryptkeyformat = keyAttrs.KEYFORMAT ?? 'identity';
|
|
927
|
+
|
|
928
|
+
if (decrypturi && keyAttrs.IV && !decryptiv) {
|
|
929
|
+
logger.error(`Invalid IV: ${keyAttrs.IV}`);
|
|
930
|
+
}
|
|
931
|
+
// If decrypturi is a URI with a scheme, then baseurl will be ignored
|
|
932
|
+
// No uri is allowed when METHOD is NONE
|
|
933
|
+
const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : '';
|
|
934
|
+
const keyFormatVersions = (
|
|
935
|
+
decryptkeyformatversions ? decryptkeyformatversions : '1'
|
|
936
|
+
)
|
|
937
|
+
.split('/')
|
|
938
|
+
.map(Number)
|
|
939
|
+
.filter(Number.isFinite);
|
|
940
|
+
|
|
941
|
+
return new LevelKey(
|
|
942
|
+
decryptmethod,
|
|
943
|
+
resolvedUri,
|
|
944
|
+
decryptkeyformat,
|
|
945
|
+
keyFormatVersions,
|
|
946
|
+
decryptiv,
|
|
947
|
+
keyAttrs.KEYID,
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function parseStartTimeOffset(startAttributes: string): number | null {
|
|
952
|
+
const startAttrs = new AttrList(startAttributes);
|
|
953
|
+
const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
|
|
954
|
+
if (Number.isFinite(startTimeOffset)) {
|
|
955
|
+
return startTimeOffset;
|
|
956
|
+
}
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function setCodecs(
|
|
961
|
+
codecsAttributeValue: string | undefined,
|
|
962
|
+
level: CodecsParsed,
|
|
963
|
+
) {
|
|
964
|
+
let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter((c) => c);
|
|
965
|
+
['video', 'audio', 'text'].forEach((type: CodecType) => {
|
|
966
|
+
const filtered = codecs.filter((codec) => isCodecType(codec, type));
|
|
967
|
+
if (filtered.length) {
|
|
968
|
+
// Comma separated list of all codecs for type
|
|
969
|
+
level[`${type}Codec`] = filtered.map((c) => c.split('/')[0]).join(',');
|
|
970
|
+
// Remove known codecs so that only unknownCodecs are left after iterating through each type
|
|
971
|
+
codecs = codecs.filter((codec) => filtered.indexOf(codec) === -1);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
level.unknownCodecs = codecs;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function assignCodec(
|
|
978
|
+
media: MediaPlaylist,
|
|
979
|
+
groupItem: { audioCodec?: string; textCodec?: string },
|
|
980
|
+
codecProperty: 'audioCodec' | 'textCodec',
|
|
981
|
+
) {
|
|
982
|
+
const codecValue = groupItem[codecProperty];
|
|
983
|
+
if (codecValue) {
|
|
984
|
+
media[codecProperty] = codecValue;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function backfillProgramDateTimes(
|
|
989
|
+
fragments: M3U8ParserFragments,
|
|
990
|
+
firstPdtIndex: number,
|
|
991
|
+
) {
|
|
992
|
+
let fragPrev = fragments[firstPdtIndex] as Fragment;
|
|
993
|
+
for (let i = firstPdtIndex; i--; ) {
|
|
994
|
+
const frag = fragments[i];
|
|
995
|
+
// Exit on delta-playlist skipped segments
|
|
996
|
+
if (!frag) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
frag.programDateTime =
|
|
1000
|
+
(fragPrev.programDateTime as number) - frag.duration * 1000;
|
|
1001
|
+
fragPrev = frag;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
export function assignProgramDateTime(
|
|
1006
|
+
frag: MediaFragment,
|
|
1007
|
+
prevFrag: MediaFragment | null,
|
|
1008
|
+
programDateTimes: MediaFragment[],
|
|
1009
|
+
) {
|
|
1010
|
+
if (frag.rawProgramDateTime) {
|
|
1011
|
+
programDateTimes.push(frag);
|
|
1012
|
+
} else if (prevFrag?.programDateTime) {
|
|
1013
|
+
frag.programDateTime = prevFrag.endProgramDateTime;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function setInitSegment(
|
|
1018
|
+
frag: Fragment,
|
|
1019
|
+
mapAttrs: AttrList,
|
|
1020
|
+
id: number,
|
|
1021
|
+
levelkeys: LevelKeys | undefined,
|
|
1022
|
+
) {
|
|
1023
|
+
frag.relurl = mapAttrs.URI;
|
|
1024
|
+
if (mapAttrs.BYTERANGE) {
|
|
1025
|
+
frag.setByteRange(mapAttrs.BYTERANGE);
|
|
1026
|
+
}
|
|
1027
|
+
frag.level = id;
|
|
1028
|
+
frag.sn = 'initSegment';
|
|
1029
|
+
if (levelkeys) {
|
|
1030
|
+
frag.levelkeys = levelkeys;
|
|
1031
|
+
}
|
|
1032
|
+
frag.initSegment = null;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function setFragLevelKeys(
|
|
1036
|
+
frag: Fragment,
|
|
1037
|
+
levelkeys: LevelKeys,
|
|
1038
|
+
level: LevelDetails,
|
|
1039
|
+
) {
|
|
1040
|
+
frag.levelkeys = levelkeys;
|
|
1041
|
+
const { encryptedFragments } = level;
|
|
1042
|
+
if (
|
|
1043
|
+
(!encryptedFragments.length ||
|
|
1044
|
+
encryptedFragments[encryptedFragments.length - 1].levelkeys !==
|
|
1045
|
+
levelkeys) &&
|
|
1046
|
+
Object.keys(levelkeys).some(
|
|
1047
|
+
(format) => levelkeys[format]!.isCommonEncryption,
|
|
1048
|
+
)
|
|
1049
|
+
) {
|
|
1050
|
+
encryptedFragments.push(frag);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function assignMultipleMediaPlaylistTagOccuranceError(
|
|
1055
|
+
level: LevelDetails,
|
|
1056
|
+
tag: string,
|
|
1057
|
+
result: string[],
|
|
1058
|
+
) {
|
|
1059
|
+
level.playlistParsingError = new Error(
|
|
1060
|
+
`#EXT-X-${tag} must not appear more than once (${result[0]})`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function assignMustAppearBeforeSegmentsError(
|
|
1065
|
+
level: LevelDetails,
|
|
1066
|
+
tag: string,
|
|
1067
|
+
result: string[],
|
|
1068
|
+
) {
|
|
1069
|
+
level.playlistParsingError = new Error(
|
|
1070
|
+
`#EXT-X-${tag} must appear before the first Media Segment (${result[0]})`,
|
|
1071
|
+
);
|
|
1072
|
+
}
|