/
fragment.ts
336 lines (294 loc) · 10.1 KB
/
fragment.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
import { buildAbsoluteURL } from 'url-toolkit';
import { logger } from '../utils/logger';
import { LevelKey } from './level-key';
import { LoadStats } from './load-stats';
import { AttrList } from '../utils/attr-list';
import type {
FragmentLoaderContext,
Loader,
PlaylistLevelType,
} from '../types/loader';
export enum ElementaryStreamTypes {
AUDIO = 'audio',
VIDEO = 'video',
AUDIOVIDEO = 'audiovideo',
}
export interface ElementaryStreamInfo {
startPTS: number;
endPTS: number;
startDTS: number;
endDTS: number;
partial?: boolean;
}
export type ElementaryStreams = Record<
ElementaryStreamTypes,
ElementaryStreamInfo | null
>;
export class BaseSegment {
private _byteRange: number[] | null = null;
private _url: string | null = null;
// baseurl is the URL to the playlist
public readonly baseurl: string;
// relurl is the portion of the URL that comes from inside the playlist.
public relurl?: string;
// Holds the types of data this fragment supports
public elementaryStreams: ElementaryStreams = {
[ElementaryStreamTypes.AUDIO]: null,
[ElementaryStreamTypes.VIDEO]: null,
[ElementaryStreamTypes.AUDIOVIDEO]: null,
};
constructor(baseurl: string) {
this.baseurl = baseurl;
}
// setByteRange converts a EXT-X-BYTERANGE attribute into a two element array
setByteRange(value: string, previous?: BaseSegment) {
const params = value.split('@', 2);
const byteRange: number[] = [];
if (params.length === 1) {
byteRange[0] = previous ? previous.byteRangeEndOffset : 0;
} else {
byteRange[0] = parseInt(params[1]);
}
byteRange[1] = parseInt(params[0]) + byteRange[0];
this._byteRange = byteRange;
}
get byteRange(): number[] {
if (!this._byteRange) {
return [];
}
return this._byteRange;
}
get byteRangeStartOffset(): number {
return this.byteRange[0];
}
get byteRangeEndOffset(): number {
return this.byteRange[1];
}
get url(): string {
if (!this._url && this.baseurl && this.relurl) {
this._url = buildAbsoluteURL(this.baseurl, this.relurl, {
alwaysNormalize: true,
});
}
return this._url || '';
}
set url(value: string) {
this._url = value;
}
}
export class Fragment extends BaseSegment {
private _decryptdata: LevelKey | null = null;
public rawProgramDateTime: string | null = null;
public programDateTime: number | null = null;
public tagList: Array<string[]> = [];
// EXTINF has to be present for a m38 to be considered valid
public duration: number = 0;
// sn notates the sequence number for a segment, and if set to a string can be 'initSegment'
public sn: number | 'initSegment' = 0;
// levelkey is the EXT-X-KEY that applies to this segment for decryption
// core difference from the private field _decryptdata is the lack of the initialized IV
// _decryptdata will set the IV for this segment based on the segment number in the fragment
public levelkey?: LevelKey;
// A string representing the fragment type
public readonly type: PlaylistLevelType;
// A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading
public loader: Loader<FragmentLoaderContext> | null = null;
// The level/track index to which the fragment belongs
public level: number = -1;
// The continuity counter of the fragment
public cc: number = 0;
// The starting Presentation Time Stamp (PTS) of the fragment. Set after transmux complete.
public startPTS?: number;
// The ending Presentation Time Stamp (PTS) of the fragment. Set after transmux complete.
public endPTS?: number;
// The latest Presentation Time Stamp (PTS) appended to the buffer.
public appendedPTS?: number;
// The starting Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
public startDTS!: number;
// The ending Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
public endDTS!: number;
// The start time of the fragment, as listed in the manifest. Updated after transmux complete.
public start: number = 0;
// Set by `updateFragPTSDTS` in level-helper
public deltaPTS?: number;
// The maximum starting Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete.
public maxStartPTS?: number;
// The minimum ending Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete.
public minEndPTS?: number;
// Load/parse timing information
public stats: LoadStats = new LoadStats();
public urlId: number = 0;
public data?: Uint8Array;
// A flag indicating whether the segment was downloaded in order to test bitrate, and was not buffered
public bitrateTest: boolean = false;
// #EXTINF segment title
public title: string | null = null;
// The Media Initialization Section for this segment
public initSegment: Fragment | null = null;
constructor(type: PlaylistLevelType, baseurl: string) {
super(baseurl);
this.type = type;
}
get decryptdata(): LevelKey | null {
if (!this.levelkey && !this._decryptdata) {
return null;
}
if (!this._decryptdata && this.levelkey) {
let sn = this.sn;
if (typeof sn !== 'number') {
// We are fetching decryption data for a initialization segment
// If the segment was encrypted with AES-128
// It must have an IV defined. We cannot substitute the Segment Number in.
if (
this.levelkey &&
this.levelkey.method === 'AES-128' &&
!this.levelkey.iv
) {
logger.warn(
`missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue`
);
}
/*
Be converted to a Number.
'initSegment' will become NaN.
NaN, which when converted through ToInt32() -> +0.
---
Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
*/
sn = 0;
}
this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn);
}
return this._decryptdata;
}
get end(): number {
return this.start + this.duration;
}
get endProgramDateTime() {
if (this.programDateTime === null) {
return null;
}
if (!Number.isFinite(this.programDateTime)) {
return null;
}
const duration = !Number.isFinite(this.duration) ? 0 : this.duration;
return this.programDateTime + duration * 1000;
}
get encrypted() {
// At the m3u8-parser level we need to add support for manifest signalled keyformats
// when we want the fragment to start reporting that it is encrypted.
// Currently, keyFormat will only be set for identity keys
if (this.decryptdata?.keyFormat && this.decryptdata.uri) {
return true;
}
return false;
}
/**
* Utility method for parseLevelPlaylist to create an initialization vector for a given segment
* @param {number} segmentNumber - segment number to generate IV with
* @returns {Uint8Array}
*/
createInitializationVector(segmentNumber: number): Uint8Array {
const uint8View = new Uint8Array(16);
for (let i = 12; i < 16; i++) {
uint8View[i] = (segmentNumber >> (8 * (15 - i))) & 0xff;
}
return uint8View;
}
/**
* Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data
* @param levelkey - a playlist's encryption info
* @param segmentNumber - the fragment's segment number
* @returns {LevelKey} - an object to be applied as a fragment's decryptdata
*/
setDecryptDataFromLevelKey(
levelkey: LevelKey,
segmentNumber: number
): LevelKey {
let decryptdata = levelkey;
if (levelkey?.method === 'AES-128' && levelkey.uri && !levelkey.iv) {
decryptdata = LevelKey.fromURI(levelkey.uri);
decryptdata.method = levelkey.method;
decryptdata.iv = this.createInitializationVector(segmentNumber);
decryptdata.keyFormat = 'identity';
}
return decryptdata;
}
setElementaryStreamInfo(
type: ElementaryStreamTypes,
startPTS: number,
endPTS: number,
startDTS: number,
endDTS: number,
partial: boolean = false
) {
const { elementaryStreams } = this;
const info = elementaryStreams[type];
if (!info) {
elementaryStreams[type] = {
startPTS,
endPTS,
startDTS,
endDTS,
partial,
};
return;
}
info.startPTS = Math.min(info.startPTS, startPTS);
info.endPTS = Math.max(info.endPTS, endPTS);
info.startDTS = Math.min(info.startDTS, startDTS);
info.endDTS = Math.max(info.endDTS, endDTS);
}
clearElementaryStreamInfo() {
const { elementaryStreams } = this;
elementaryStreams[ElementaryStreamTypes.AUDIO] = null;
elementaryStreams[ElementaryStreamTypes.VIDEO] = null;
elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO] = null;
}
}
export class Part extends BaseSegment {
public readonly fragOffset: number = 0;
public readonly duration: number = 0;
public readonly gap: boolean = false;
public readonly independent: boolean = false;
public readonly relurl: string;
public readonly fragment: Fragment;
public readonly index: number;
public stats: LoadStats = new LoadStats();
constructor(
partAttrs: AttrList,
frag: Fragment,
baseurl: string,
index: number,
previous?: Part
) {
super(baseurl);
this.duration = partAttrs.decimalFloatingPoint('DURATION');
this.gap = partAttrs.bool('GAP');
this.independent = partAttrs.bool('INDEPENDENT');
this.relurl = partAttrs.enumeratedString('URI') as string;
this.fragment = frag;
this.index = index;
const byteRange = partAttrs.enumeratedString('BYTERANGE');
if (byteRange) {
this.setByteRange(byteRange, previous);
}
if (previous) {
this.fragOffset = previous.fragOffset + previous.duration;
}
}
get start(): number {
return this.fragment.start + this.fragOffset;
}
get end(): number {
return this.start + this.duration;
}
get loaded(): boolean {
const { elementaryStreams } = this;
return !!(
elementaryStreams.audio ||
elementaryStreams.video ||
elementaryStreams.audiovideo
);
}
}