@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,677 @@
|
|
|
1
|
+
import { Events } from '../events';
|
|
2
|
+
import { PlaylistLevelType } from '../types/loader';
|
|
3
|
+
import Cea608Parser from '../utils/cea-608-parser';
|
|
4
|
+
import { IMSC1_CODEC, parseIMSC1 } from '../utils/imsc1-ttml-parser';
|
|
5
|
+
import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
|
|
6
|
+
import { appendUint8Array } from '../utils/mp4-tools';
|
|
7
|
+
import OutputFilter from '../utils/output-filter';
|
|
8
|
+
import {
|
|
9
|
+
addCueToTrack,
|
|
10
|
+
createTrackNode,
|
|
11
|
+
removeCuesInRange,
|
|
12
|
+
} from '../utils/texttrack-utils';
|
|
13
|
+
import { parseWebVTT } from '../utils/webvtt-parser';
|
|
14
|
+
import type { HlsConfig } from '../config';
|
|
15
|
+
import type Hls from '../hls';
|
|
16
|
+
import type { Fragment } from '../loader/fragment';
|
|
17
|
+
import type { ComponentAPI } from '../types/component-api';
|
|
18
|
+
import type {
|
|
19
|
+
BufferFlushingData,
|
|
20
|
+
FragDecryptedData,
|
|
21
|
+
FragLoadedData,
|
|
22
|
+
FragLoadingData,
|
|
23
|
+
FragParsingUserdataData,
|
|
24
|
+
InitPTSFoundData,
|
|
25
|
+
ManifestLoadedData,
|
|
26
|
+
MediaAttachingData,
|
|
27
|
+
MediaDetachingData,
|
|
28
|
+
SubtitleTracksUpdatedData,
|
|
29
|
+
} from '../types/events';
|
|
30
|
+
import type { MediaPlaylist } from '../types/media-playlist';
|
|
31
|
+
import type { VTTCCs } from '../types/vtt';
|
|
32
|
+
import type { CaptionScreen } from '../utils/cea-608-parser';
|
|
33
|
+
import type { CuesInterface } from '../utils/cues';
|
|
34
|
+
import type { TimestampOffset } from '../utils/timescale-conversion';
|
|
35
|
+
|
|
36
|
+
type TrackProperties = {
|
|
37
|
+
label: string;
|
|
38
|
+
languageCode: string;
|
|
39
|
+
media?: MediaPlaylist;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type NonNativeCaptionsTrack = {
|
|
43
|
+
_id?: string;
|
|
44
|
+
label: string;
|
|
45
|
+
kind: string;
|
|
46
|
+
default: boolean;
|
|
47
|
+
closedCaptions?: MediaPlaylist;
|
|
48
|
+
subtitleTrack?: MediaPlaylist;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export class TimelineController implements ComponentAPI {
|
|
52
|
+
private hls: Hls;
|
|
53
|
+
private media: HTMLMediaElement | null = null;
|
|
54
|
+
private config: HlsConfig;
|
|
55
|
+
private enabled: boolean = true;
|
|
56
|
+
private Cues: CuesInterface;
|
|
57
|
+
private tracks: Array<MediaPlaylist> = [];
|
|
58
|
+
private initPTS: TimestampOffset[] = [];
|
|
59
|
+
private unparsedVttFrags: Array<FragLoadedData | FragDecryptedData> = [];
|
|
60
|
+
private captionsTracks: Record<string, HTMLTrackElement> = {};
|
|
61
|
+
private nonNativeCaptionsTracks: Record<string, NonNativeCaptionsTrack> = {};
|
|
62
|
+
private cea608Parser1?: Cea608Parser;
|
|
63
|
+
private cea608Parser2?: Cea608Parser;
|
|
64
|
+
private lastCc: number = -1; // Last video (CEA-608) fragment CC
|
|
65
|
+
private lastSn: number = -1; // Last video (CEA-608) fragment MSN
|
|
66
|
+
private lastPartIndex: number = -1; // Last video (CEA-608) fragment Part Index
|
|
67
|
+
private prevCC: number = -1; // Last subtitle fragment CC
|
|
68
|
+
private vttCCs: VTTCCs = newVTTCCs();
|
|
69
|
+
private captionsProperties: {
|
|
70
|
+
textTrack1: TrackProperties;
|
|
71
|
+
textTrack2: TrackProperties;
|
|
72
|
+
textTrack3: TrackProperties;
|
|
73
|
+
textTrack4: TrackProperties;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
constructor(hls: Hls) {
|
|
77
|
+
this.hls = hls;
|
|
78
|
+
this.config = hls.config;
|
|
79
|
+
this.Cues = hls.config.cueHandler;
|
|
80
|
+
|
|
81
|
+
this.captionsProperties = {
|
|
82
|
+
textTrack1: {
|
|
83
|
+
label: this.config.captionsTextTrack1Label,
|
|
84
|
+
languageCode: this.config.captionsTextTrack1LanguageCode,
|
|
85
|
+
},
|
|
86
|
+
textTrack2: {
|
|
87
|
+
label: this.config.captionsTextTrack2Label,
|
|
88
|
+
languageCode: this.config.captionsTextTrack2LanguageCode,
|
|
89
|
+
},
|
|
90
|
+
textTrack3: {
|
|
91
|
+
label: this.config.captionsTextTrack3Label,
|
|
92
|
+
languageCode: this.config.captionsTextTrack3LanguageCode,
|
|
93
|
+
},
|
|
94
|
+
textTrack4: {
|
|
95
|
+
label: this.config.captionsTextTrack4Label,
|
|
96
|
+
languageCode: this.config.captionsTextTrack4LanguageCode,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
|
|
101
|
+
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
|
102
|
+
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
103
|
+
hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
|
104
|
+
hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
|
|
105
|
+
hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
|
|
106
|
+
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
|
|
107
|
+
hls.on(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this);
|
|
108
|
+
hls.on(Events.FRAG_DECRYPTED, this.onFragDecrypted, this);
|
|
109
|
+
hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
|
|
110
|
+
hls.on(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this);
|
|
111
|
+
hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public destroy(): void {
|
|
115
|
+
const { hls } = this;
|
|
116
|
+
hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
|
|
117
|
+
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
|
118
|
+
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
119
|
+
hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
|
120
|
+
hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
|
|
121
|
+
hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
|
|
122
|
+
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
|
|
123
|
+
hls.off(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this);
|
|
124
|
+
hls.off(Events.FRAG_DECRYPTED, this.onFragDecrypted, this);
|
|
125
|
+
hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
|
|
126
|
+
hls.off(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this);
|
|
127
|
+
hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
|
|
128
|
+
// @ts-ignore
|
|
129
|
+
this.hls = this.config = this.media = null;
|
|
130
|
+
this.cea608Parser1 = this.cea608Parser2 = undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private initCea608Parsers() {
|
|
134
|
+
const channel1 = new OutputFilter(this, 'textTrack1');
|
|
135
|
+
const channel2 = new OutputFilter(this, 'textTrack2');
|
|
136
|
+
const channel3 = new OutputFilter(this, 'textTrack3');
|
|
137
|
+
const channel4 = new OutputFilter(this, 'textTrack4');
|
|
138
|
+
this.cea608Parser1 = new Cea608Parser(1, channel1, channel2);
|
|
139
|
+
this.cea608Parser2 = new Cea608Parser(3, channel3, channel4);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public addCues(
|
|
143
|
+
trackName: string,
|
|
144
|
+
startTime: number,
|
|
145
|
+
endTime: number,
|
|
146
|
+
screen: CaptionScreen,
|
|
147
|
+
cueRanges: Array<[number, number]>,
|
|
148
|
+
) {
|
|
149
|
+
// skip cues which overlap more than 50% with previously parsed time ranges
|
|
150
|
+
let merged = false;
|
|
151
|
+
for (let i = cueRanges.length; i--; ) {
|
|
152
|
+
const cueRange = cueRanges[i];
|
|
153
|
+
const overlap = intersection(
|
|
154
|
+
cueRange[0],
|
|
155
|
+
cueRange[1],
|
|
156
|
+
startTime,
|
|
157
|
+
endTime,
|
|
158
|
+
);
|
|
159
|
+
if (overlap >= 0) {
|
|
160
|
+
cueRange[0] = Math.min(cueRange[0], startTime);
|
|
161
|
+
cueRange[1] = Math.max(cueRange[1], endTime);
|
|
162
|
+
merged = true;
|
|
163
|
+
if (overlap / (endTime - startTime) > 0.5) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!merged) {
|
|
169
|
+
cueRanges.push([startTime, endTime]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (this.config.renderTextTracksNatively) {
|
|
173
|
+
const track = this.captionsTracks[trackName].track;
|
|
174
|
+
this.Cues.newCue(track, startTime, endTime, screen);
|
|
175
|
+
} else {
|
|
176
|
+
const cues = this.Cues.newCue(null, startTime, endTime, screen);
|
|
177
|
+
this.hls.trigger(Events.CUES_PARSED, {
|
|
178
|
+
type: 'captions',
|
|
179
|
+
cues,
|
|
180
|
+
track: trackName,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Triggered when an initial PTS is found; used for synchronisation of WebVTT.
|
|
186
|
+
private onInitPtsFound(
|
|
187
|
+
event: Events.INIT_PTS_FOUND,
|
|
188
|
+
{ frag, id, initPTS, timescale, trackId }: InitPTSFoundData,
|
|
189
|
+
) {
|
|
190
|
+
const { unparsedVttFrags } = this;
|
|
191
|
+
if (id === PlaylistLevelType.MAIN) {
|
|
192
|
+
this.initPTS[frag.cc] = { baseTime: initPTS, timescale, trackId };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Due to asynchronous processing, initial PTS may arrive later than the first VTT fragments are loaded.
|
|
196
|
+
// Parse any unparsed fragments upon receiving the initial PTS.
|
|
197
|
+
if (unparsedVttFrags.length) {
|
|
198
|
+
this.unparsedVttFrags = [];
|
|
199
|
+
unparsedVttFrags.forEach((data) => {
|
|
200
|
+
if (this.initPTS[data.frag.cc]) {
|
|
201
|
+
this.onFragLoaded(Events.FRAG_LOADED, data as FragLoadedData);
|
|
202
|
+
} else {
|
|
203
|
+
this.hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, {
|
|
204
|
+
success: false,
|
|
205
|
+
frag: data.frag,
|
|
206
|
+
part: null,
|
|
207
|
+
error: new Error(
|
|
208
|
+
'Subtitle discontinuity domain does not match main',
|
|
209
|
+
),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
public createCaptionsTrack(trackName: string) {
|
|
217
|
+
if (this.config.renderTextTracksNatively) {
|
|
218
|
+
this.createNativeTrack(trackName);
|
|
219
|
+
} else {
|
|
220
|
+
this.createNonNativeTrack(trackName);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private createNativeTrack(trackName: string) {
|
|
225
|
+
if (this.captionsTracks[trackName]) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const { captionsProperties, captionsTracks, media } = this;
|
|
229
|
+
const { label, languageCode } = captionsProperties[trackName];
|
|
230
|
+
if (media) {
|
|
231
|
+
captionsTracks[trackName] = createTrackNode(
|
|
232
|
+
media,
|
|
233
|
+
'captions',
|
|
234
|
+
label,
|
|
235
|
+
languageCode,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private createNonNativeTrack(trackName: string) {
|
|
241
|
+
if (this.nonNativeCaptionsTracks[trackName]) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Create a list of a single track for the provider to consume
|
|
245
|
+
const trackProperties: TrackProperties = this.captionsProperties[trackName];
|
|
246
|
+
if (!trackProperties) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const label = trackProperties.label as string;
|
|
250
|
+
const track = {
|
|
251
|
+
_id: trackName,
|
|
252
|
+
label,
|
|
253
|
+
kind: 'captions',
|
|
254
|
+
default: trackProperties.media ? !!trackProperties.media.default : false,
|
|
255
|
+
closedCaptions: trackProperties.media,
|
|
256
|
+
};
|
|
257
|
+
this.nonNativeCaptionsTracks[trackName] = track;
|
|
258
|
+
this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { tracks: [track] });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private onMediaAttaching(
|
|
262
|
+
event: Events.MEDIA_ATTACHING,
|
|
263
|
+
data: MediaAttachingData,
|
|
264
|
+
) {
|
|
265
|
+
this.media = data.media;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private onMediaDetaching(
|
|
269
|
+
event: Events.MEDIA_DETACHING,
|
|
270
|
+
data: MediaDetachingData,
|
|
271
|
+
) {
|
|
272
|
+
const transferringMedia = !!data.transferMedia;
|
|
273
|
+
this.media = null;
|
|
274
|
+
if (transferringMedia) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (this.config.renderTextTracksNatively) {
|
|
279
|
+
const { captionsTracks } = this;
|
|
280
|
+
for (const trackName in captionsTracks) {
|
|
281
|
+
captionsTracks[trackName].remove();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.captionsTracks = {};
|
|
285
|
+
this.nonNativeCaptionsTracks = {};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private onManifestLoading() {
|
|
289
|
+
// Detect discontinuity in video fragment (CEA-608) parsing
|
|
290
|
+
this.lastCc = -1;
|
|
291
|
+
this.lastSn = -1;
|
|
292
|
+
this.lastPartIndex = -1;
|
|
293
|
+
// Detect discontinuity in subtitle manifests
|
|
294
|
+
this.prevCC = -1;
|
|
295
|
+
this.vttCCs = newVTTCCs();
|
|
296
|
+
this.tracks = [];
|
|
297
|
+
this.captionsTracks = {};
|
|
298
|
+
this.nonNativeCaptionsTracks = {};
|
|
299
|
+
this.unparsedVttFrags = [];
|
|
300
|
+
this.initPTS = [];
|
|
301
|
+
if (this.cea608Parser1 && this.cea608Parser2) {
|
|
302
|
+
this.cea608Parser1.reset();
|
|
303
|
+
this.cea608Parser2.reset();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private onSubtitleTracksUpdated(
|
|
308
|
+
event: Events.SUBTITLE_TRACKS_UPDATED,
|
|
309
|
+
data: SubtitleTracksUpdatedData,
|
|
310
|
+
) {
|
|
311
|
+
const tracks: Array<MediaPlaylist> = data.subtitleTracks || [];
|
|
312
|
+
if (
|
|
313
|
+
tracks.length &&
|
|
314
|
+
!this.config.renderTextTracksNatively &&
|
|
315
|
+
!subtitleOptionsIdentical(this.tracks, tracks)
|
|
316
|
+
) {
|
|
317
|
+
// Create a list of tracks for the provider to consume
|
|
318
|
+
const tracksList = tracks.map((track) => {
|
|
319
|
+
return {
|
|
320
|
+
label: track.name,
|
|
321
|
+
kind: track.type.toLowerCase(),
|
|
322
|
+
default: track.default,
|
|
323
|
+
subtitleTrack: track,
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, {
|
|
327
|
+
tracks: tracksList,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
this.tracks = tracks;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private onManifestLoaded(
|
|
334
|
+
event: Events.MANIFEST_LOADED,
|
|
335
|
+
data: ManifestLoadedData,
|
|
336
|
+
) {
|
|
337
|
+
if (this.config.enableCEA708Captions && data.captions) {
|
|
338
|
+
data.captions.forEach((captionsTrack) => {
|
|
339
|
+
const instreamIdMatch = /(?:CC|SERVICE)([1-4])/.exec(
|
|
340
|
+
captionsTrack.instreamId as string,
|
|
341
|
+
);
|
|
342
|
+
if (!instreamIdMatch) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const trackName = `textTrack${instreamIdMatch[1]}`;
|
|
346
|
+
const trackProperties: TrackProperties =
|
|
347
|
+
this.captionsProperties[trackName];
|
|
348
|
+
if (!trackProperties) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
trackProperties.label = captionsTrack.name;
|
|
352
|
+
if (captionsTrack.lang) {
|
|
353
|
+
// optional attribute
|
|
354
|
+
trackProperties.languageCode = captionsTrack.lang;
|
|
355
|
+
}
|
|
356
|
+
trackProperties.media = captionsTrack;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private closedCaptionsForLevel(frag: Fragment): string | undefined {
|
|
362
|
+
const level = this.hls.levels[frag.level];
|
|
363
|
+
return level?.attrs['CLOSED-CAPTIONS'];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
|
|
367
|
+
// if this frag isn't contiguous, clear the parser so cues with bad start/end times aren't added to the textTrack
|
|
368
|
+
if (this.enabled && data.frag.type === PlaylistLevelType.MAIN) {
|
|
369
|
+
const { cea608Parser1, cea608Parser2, lastSn } = this;
|
|
370
|
+
const { cc, sn } = data.frag;
|
|
371
|
+
const partIndex = data.part?.index ?? -1;
|
|
372
|
+
if (cea608Parser1 && cea608Parser2) {
|
|
373
|
+
if (
|
|
374
|
+
sn !== lastSn + 1 ||
|
|
375
|
+
(sn === lastSn && partIndex !== this.lastPartIndex + 1) ||
|
|
376
|
+
cc !== this.lastCc
|
|
377
|
+
) {
|
|
378
|
+
cea608Parser1.reset();
|
|
379
|
+
cea608Parser2.reset();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
this.lastCc = cc as number;
|
|
383
|
+
this.lastSn = sn as number;
|
|
384
|
+
this.lastPartIndex = partIndex;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private onFragLoaded(
|
|
389
|
+
event: Events.FRAG_LOADED,
|
|
390
|
+
data: FragDecryptedData | FragLoadedData,
|
|
391
|
+
) {
|
|
392
|
+
const { frag, payload } = data;
|
|
393
|
+
if (
|
|
394
|
+
frag.level < this.tracks.length &&
|
|
395
|
+
frag.type === PlaylistLevelType.SUBTITLE
|
|
396
|
+
) {
|
|
397
|
+
// If fragment is subtitle type, parse as WebVTT.
|
|
398
|
+
if (payload.byteLength) {
|
|
399
|
+
const decryptData = frag.decryptdata;
|
|
400
|
+
// fragment after decryption has a stats object
|
|
401
|
+
const decrypted = 'stats' in data;
|
|
402
|
+
// If the subtitles are not encrypted, parse VTTs now. Otherwise, we need to wait.
|
|
403
|
+
if (decryptData == null || !decryptData.encrypted || decrypted) {
|
|
404
|
+
const trackPlaylistMedia = this.tracks[frag.level] as
|
|
405
|
+
| MediaPlaylist
|
|
406
|
+
| undefined;
|
|
407
|
+
const vttCCs = this.vttCCs;
|
|
408
|
+
if (!vttCCs[frag.cc]) {
|
|
409
|
+
vttCCs[frag.cc] = {
|
|
410
|
+
start: frag.start,
|
|
411
|
+
prevCC: this.prevCC,
|
|
412
|
+
new: true,
|
|
413
|
+
};
|
|
414
|
+
this.prevCC = frag.cc;
|
|
415
|
+
}
|
|
416
|
+
if (trackPlaylistMedia?.textCodec === IMSC1_CODEC) {
|
|
417
|
+
this._parseIMSC1(data);
|
|
418
|
+
} else {
|
|
419
|
+
this._parseVTTs(data);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// In case there is no payload, finish unsuccessfully.
|
|
424
|
+
const part = 'part' in data ? data.part : null;
|
|
425
|
+
this.hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, {
|
|
426
|
+
success: false,
|
|
427
|
+
frag,
|
|
428
|
+
part,
|
|
429
|
+
error: new Error('Empty subtitle payload'),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private _parseIMSC1(data: FragDecryptedData | FragLoadedData) {
|
|
436
|
+
const { frag, payload } = data;
|
|
437
|
+
const part = 'part' in data ? data.part : null;
|
|
438
|
+
const hls = this.hls;
|
|
439
|
+
parseIMSC1(
|
|
440
|
+
payload,
|
|
441
|
+
this.initPTS[frag.cc],
|
|
442
|
+
(cues) => {
|
|
443
|
+
this._appendCues(cues, frag.level);
|
|
444
|
+
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, {
|
|
445
|
+
success: true,
|
|
446
|
+
frag,
|
|
447
|
+
part,
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
(error) => {
|
|
451
|
+
hls.logger.log(`Failed to parse IMSC1: ${error}`);
|
|
452
|
+
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, {
|
|
453
|
+
success: false,
|
|
454
|
+
frag,
|
|
455
|
+
part,
|
|
456
|
+
error,
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private _parseVTTs(data: FragDecryptedData | FragLoadedData) {
|
|
463
|
+
const { frag, payload } = data;
|
|
464
|
+
const part = 'part' in data ? data.part : null;
|
|
465
|
+
// We need an initial synchronisation PTS. Store fragments as long as none has arrived
|
|
466
|
+
const { initPTS, unparsedVttFrags } = this;
|
|
467
|
+
const maxAvCC = initPTS.length - 1;
|
|
468
|
+
if (!initPTS[frag.cc] && maxAvCC === -1) {
|
|
469
|
+
unparsedVttFrags.push(data);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const hls = this.hls;
|
|
474
|
+
// Parse the WebVTT file contents.
|
|
475
|
+
const vttHeader = frag.initSegment?.data;
|
|
476
|
+
const payloadWebVTT = vttHeader
|
|
477
|
+
? appendUint8Array(vttHeader, new Uint8Array(payload)).buffer
|
|
478
|
+
: payload;
|
|
479
|
+
parseWebVTT(
|
|
480
|
+
payloadWebVTT,
|
|
481
|
+
this.initPTS[frag.cc],
|
|
482
|
+
this.vttCCs,
|
|
483
|
+
frag.cc,
|
|
484
|
+
(part && !vttHeader ? part : frag).start,
|
|
485
|
+
(cues) => {
|
|
486
|
+
this._appendCues(cues, frag.level);
|
|
487
|
+
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, {
|
|
488
|
+
success: true,
|
|
489
|
+
frag,
|
|
490
|
+
part,
|
|
491
|
+
});
|
|
492
|
+
},
|
|
493
|
+
(error) => {
|
|
494
|
+
const missingInitPTS =
|
|
495
|
+
error.message === 'Missing initPTS for VTT MPEGTS';
|
|
496
|
+
if (missingInitPTS) {
|
|
497
|
+
unparsedVttFrags.push(data);
|
|
498
|
+
} else if (this.config.enableIMSC1) {
|
|
499
|
+
this._fallbackToIMSC1(data);
|
|
500
|
+
}
|
|
501
|
+
// Something went wrong while parsing. Trigger event with success false.
|
|
502
|
+
hls.logger.log(`Failed to parse VTT cue: ${error}`);
|
|
503
|
+
if (missingInitPTS && maxAvCC > frag.cc) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, {
|
|
507
|
+
success: false,
|
|
508
|
+
frag,
|
|
509
|
+
part,
|
|
510
|
+
error,
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private _fallbackToIMSC1(data: FragDecryptedData | FragLoadedData) {
|
|
517
|
+
const { frag, payload } = data;
|
|
518
|
+
// If textCodec is unknown, try parsing as IMSC1. Set textCodec based on the result
|
|
519
|
+
const trackPlaylistMedia = this.tracks[frag.level];
|
|
520
|
+
if (!trackPlaylistMedia.textCodec) {
|
|
521
|
+
parseIMSC1(
|
|
522
|
+
payload,
|
|
523
|
+
this.initPTS[frag.cc],
|
|
524
|
+
() => {
|
|
525
|
+
trackPlaylistMedia.textCodec = IMSC1_CODEC;
|
|
526
|
+
this._parseIMSC1(data);
|
|
527
|
+
},
|
|
528
|
+
() => {
|
|
529
|
+
trackPlaylistMedia.textCodec = 'wvtt';
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private _appendCues(cues: VTTCue[], fragLevel: number) {
|
|
536
|
+
const hls = this.hls;
|
|
537
|
+
if (this.config.renderTextTracksNatively) {
|
|
538
|
+
const textTrack = this.tracks[fragLevel].trackNode?.track;
|
|
539
|
+
// WebVTTParser.parse is an async method and if the currently selected text track mode is set to "disabled"
|
|
540
|
+
// before parsing is done then don't try to access currentTrack.cues.getCueById as cues will be null
|
|
541
|
+
// and trying to access getCueById method of cues will throw an exception
|
|
542
|
+
// Because we check if the mode is disabled, we can force check `cues` below. They can't be null.
|
|
543
|
+
if (!textTrack || textTrack.mode === 'disabled') {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
cues.forEach((cue) => addCueToTrack(textTrack, cue));
|
|
547
|
+
} else {
|
|
548
|
+
const currentTrack = this.tracks[fragLevel];
|
|
549
|
+
if (!currentTrack) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const track = currentTrack.default ? 'default' : 'subtitles' + fragLevel;
|
|
553
|
+
hls.trigger(Events.CUES_PARSED, { type: 'subtitles', cues, track });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private onFragDecrypted(
|
|
558
|
+
event: Events.FRAG_DECRYPTED,
|
|
559
|
+
data: FragDecryptedData,
|
|
560
|
+
) {
|
|
561
|
+
const { frag } = data;
|
|
562
|
+
if (frag.type === PlaylistLevelType.SUBTITLE) {
|
|
563
|
+
this.onFragLoaded(Events.FRAG_LOADED, data as unknown as FragLoadedData);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private onSubtitleTracksCleared() {
|
|
568
|
+
this.tracks = [];
|
|
569
|
+
this.captionsTracks = {};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private onFragParsingUserdata(
|
|
573
|
+
event: Events.FRAG_PARSING_USERDATA,
|
|
574
|
+
data: FragParsingUserdataData,
|
|
575
|
+
) {
|
|
576
|
+
if (!this.enabled || !this.config.enableCEA708Captions) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const { frag, samples } = data;
|
|
580
|
+
if (
|
|
581
|
+
frag.type === PlaylistLevelType.MAIN &&
|
|
582
|
+
this.closedCaptionsForLevel(frag) === 'NONE'
|
|
583
|
+
) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// If the event contains captions (found in the bytes property), push all bytes into the parser immediately
|
|
587
|
+
// It will create the proper timestamps based on the PTS value
|
|
588
|
+
for (let i = 0; i < samples.length; i++) {
|
|
589
|
+
const ccBytes = samples[i].bytes;
|
|
590
|
+
if (ccBytes) {
|
|
591
|
+
if (!this.cea608Parser1) {
|
|
592
|
+
this.initCea608Parsers();
|
|
593
|
+
}
|
|
594
|
+
const ccdatas = this.extractCea608Data(ccBytes);
|
|
595
|
+
this.cea608Parser1!.addData(samples[i].pts, ccdatas[0]);
|
|
596
|
+
this.cea608Parser2!.addData(samples[i].pts, ccdatas[1]);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
onBufferFlushing(
|
|
602
|
+
event: Events.BUFFER_FLUSHING,
|
|
603
|
+
{ startOffset, endOffset, endOffsetSubtitles, type }: BufferFlushingData,
|
|
604
|
+
) {
|
|
605
|
+
const { media } = this;
|
|
606
|
+
if (!media || media.currentTime < endOffset) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// Clear 608 caption cues from the captions TextTracks when the video back buffer is flushed
|
|
610
|
+
// Forward cues are never removed because we can loose streamed 608 content from recent fragments
|
|
611
|
+
if (!type || type === 'video') {
|
|
612
|
+
const { captionsTracks } = this;
|
|
613
|
+
Object.keys(captionsTracks).forEach((trackName) =>
|
|
614
|
+
removeCuesInRange(
|
|
615
|
+
captionsTracks[trackName].track,
|
|
616
|
+
startOffset,
|
|
617
|
+
endOffset,
|
|
618
|
+
),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
if (this.config.renderTextTracksNatively) {
|
|
622
|
+
// Clear VTT/IMSC1 subtitle cues from the subtitle TextTracks when the back buffer is flushed
|
|
623
|
+
if (startOffset === 0 && endOffsetSubtitles !== undefined) {
|
|
624
|
+
this.tracks.forEach((track) => {
|
|
625
|
+
const textTrack = track.trackNode?.track;
|
|
626
|
+
if (textTrack) {
|
|
627
|
+
removeCuesInRange(textTrack, startOffset, endOffsetSubtitles);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private extractCea608Data(byteArray: Uint8Array): number[][] {
|
|
635
|
+
const actualCCBytes: number[][] = [[], []];
|
|
636
|
+
const count = byteArray[0] & 0x1f;
|
|
637
|
+
let position = 2;
|
|
638
|
+
|
|
639
|
+
for (let j = 0; j < count; j++) {
|
|
640
|
+
const tmpByte = byteArray[position++];
|
|
641
|
+
const ccbyte1 = 0x7f & byteArray[position++];
|
|
642
|
+
const ccbyte2 = 0x7f & byteArray[position++];
|
|
643
|
+
if (ccbyte1 === 0 && ccbyte2 === 0) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const ccValid = (0x04 & tmpByte) !== 0; // Support all four channels
|
|
647
|
+
if (ccValid) {
|
|
648
|
+
const ccType = 0x03 & tmpByte;
|
|
649
|
+
if (
|
|
650
|
+
0x00 /* CEA608 field1*/ === ccType ||
|
|
651
|
+
0x01 /* CEA608 field2*/ === ccType
|
|
652
|
+
) {
|
|
653
|
+
// Exclude CEA708 CC data.
|
|
654
|
+
actualCCBytes[ccType].push(ccbyte1);
|
|
655
|
+
actualCCBytes[ccType].push(ccbyte2);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return actualCCBytes;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function intersection(x1: number, x2: number, y1: number, y2: number): number {
|
|
664
|
+
return Math.min(x2, y2) - Math.max(x1, y1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function newVTTCCs(): VTTCCs {
|
|
668
|
+
return {
|
|
669
|
+
ccOffset: 0,
|
|
670
|
+
presentationOffset: 0,
|
|
671
|
+
0: {
|
|
672
|
+
start: 0,
|
|
673
|
+
prevCC: -1,
|
|
674
|
+
new: true,
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DecrypterAesMode } from './decrypter-aes-mode';
|
|
2
|
+
|
|
3
|
+
export default class AESCrypto {
|
|
4
|
+
private subtle: SubtleCrypto;
|
|
5
|
+
private aesIV: Uint8Array<ArrayBuffer>;
|
|
6
|
+
private aesMode: DecrypterAesMode;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
subtle: SubtleCrypto,
|
|
10
|
+
iv: Uint8Array<ArrayBuffer>,
|
|
11
|
+
aesMode: DecrypterAesMode,
|
|
12
|
+
) {
|
|
13
|
+
this.subtle = subtle;
|
|
14
|
+
this.aesIV = iv;
|
|
15
|
+
this.aesMode = aesMode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
decrypt(data: ArrayBuffer, key: CryptoKey) {
|
|
19
|
+
switch (this.aesMode) {
|
|
20
|
+
case DecrypterAesMode.cbc:
|
|
21
|
+
return this.subtle.decrypt(
|
|
22
|
+
{ name: 'AES-CBC', iv: this.aesIV },
|
|
23
|
+
key,
|
|
24
|
+
data,
|
|
25
|
+
);
|
|
26
|
+
case DecrypterAesMode.ctr:
|
|
27
|
+
return this.subtle.decrypt(
|
|
28
|
+
{ name: 'AES-CTR', counter: this.aesIV, length: 64 }, //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block
|
|
29
|
+
key,
|
|
30
|
+
data,
|
|
31
|
+
);
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|