@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,617 @@
|
|
|
1
|
+
import BasePlaylistController from './base-playlist-controller';
|
|
2
|
+
import { Events } from '../events';
|
|
3
|
+
import { PlaylistContextType } from '../types/loader';
|
|
4
|
+
import { IMSC1_CODEC } from '../utils/imsc1-ttml-parser';
|
|
5
|
+
import { mediaAttributesIdentical } from '../utils/media-option-attributes';
|
|
6
|
+
import { findMatchingOption, matchesOption } from '../utils/rendition-helper';
|
|
7
|
+
import { createTrackNode, getTrackKind } from '../utils/texttrack-utils';
|
|
8
|
+
import type Hls from '../hls';
|
|
9
|
+
import type {
|
|
10
|
+
ErrorData,
|
|
11
|
+
LevelLoadingData,
|
|
12
|
+
LevelSwitchingData,
|
|
13
|
+
ManifestParsedData,
|
|
14
|
+
MediaAttachedData,
|
|
15
|
+
MediaDetachingData,
|
|
16
|
+
SubtitleTracksUpdatedData,
|
|
17
|
+
TrackLoadedData,
|
|
18
|
+
} from '../types/events';
|
|
19
|
+
import type { HlsUrlParameters } from '../types/level';
|
|
20
|
+
import type {
|
|
21
|
+
MediaPlaylist,
|
|
22
|
+
SubtitleSelectionOption,
|
|
23
|
+
} from '../types/media-playlist';
|
|
24
|
+
|
|
25
|
+
class SubtitleTrackController extends BasePlaylistController {
|
|
26
|
+
private media: HTMLMediaElement | null = null;
|
|
27
|
+
private tracks: MediaPlaylist[] = [];
|
|
28
|
+
private groupIds: (string | undefined)[] | null = null;
|
|
29
|
+
private tracksInGroup: MediaPlaylist[] = [];
|
|
30
|
+
private trackId: number = -1;
|
|
31
|
+
private currentTrack: MediaPlaylist | null = null;
|
|
32
|
+
private selectDefaultTrack: boolean = true;
|
|
33
|
+
private queuedDefaultTrack: number = -1;
|
|
34
|
+
private useTextTrackPolling: boolean = false;
|
|
35
|
+
private subtitlePollingInterval: number = -1;
|
|
36
|
+
private _subtitleDisplay: boolean = true;
|
|
37
|
+
|
|
38
|
+
private asyncPollTrackChange = () => this.pollTrackChange(0);
|
|
39
|
+
|
|
40
|
+
constructor(hls: Hls) {
|
|
41
|
+
super(hls, 'subtitle-track-controller');
|
|
42
|
+
this.registerListeners();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public destroy() {
|
|
46
|
+
this.unregisterListeners();
|
|
47
|
+
this.tracks.length = 0;
|
|
48
|
+
this.tracksInGroup.length = 0;
|
|
49
|
+
this.currentTrack = null;
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
this.onTextTracksChanged = this.asyncPollTrackChange = null;
|
|
52
|
+
super.destroy();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public get subtitleDisplay(): boolean {
|
|
56
|
+
return this._subtitleDisplay;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public set subtitleDisplay(value: boolean) {
|
|
60
|
+
this._subtitleDisplay = value;
|
|
61
|
+
if (this.trackId > -1) {
|
|
62
|
+
this.toggleTrackModes();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private registerListeners() {
|
|
67
|
+
const { hls } = this;
|
|
68
|
+
hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
|
|
69
|
+
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
|
70
|
+
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
71
|
+
hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
|
|
72
|
+
hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
|
|
73
|
+
hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
|
|
74
|
+
hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
|
|
75
|
+
hls.on(Events.ERROR, this.onError, this);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private unregisterListeners() {
|
|
79
|
+
const { hls } = this;
|
|
80
|
+
hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
|
|
81
|
+
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
|
82
|
+
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
83
|
+
hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
|
|
84
|
+
hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this);
|
|
85
|
+
hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
|
|
86
|
+
hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
|
|
87
|
+
hls.off(Events.ERROR, this.onError, this);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private createTracksInGroup() {
|
|
91
|
+
if (!this.media || !this.hls.config.renderTextTracksNatively) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.tracksInGroup.forEach((track) => {
|
|
95
|
+
if (!track.trackNode) {
|
|
96
|
+
track.trackNode = createTrackNode(
|
|
97
|
+
this.media!,
|
|
98
|
+
getTrackKind(track),
|
|
99
|
+
track.name,
|
|
100
|
+
track.lang,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const track = this.currentTrack?.trackNode?.track;
|
|
105
|
+
// new tracks are disable before appending
|
|
106
|
+
if (track?.mode === 'disabled') {
|
|
107
|
+
track.mode = this._subtitleDisplay ? 'showing' : 'hidden';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Listen for subtitle track change, then extract the current track ID.
|
|
112
|
+
protected onMediaAttached(
|
|
113
|
+
event: Events.MEDIA_ATTACHED,
|
|
114
|
+
data: MediaAttachedData,
|
|
115
|
+
): void {
|
|
116
|
+
const media = data.media;
|
|
117
|
+
this.media = media;
|
|
118
|
+
|
|
119
|
+
let trackId = this.trackId;
|
|
120
|
+
if (this.queuedDefaultTrack > -1) {
|
|
121
|
+
trackId = this.queuedDefaultTrack;
|
|
122
|
+
this.queuedDefaultTrack = -1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.setSubtitleTrack(trackId);
|
|
126
|
+
this.createTracksInGroup();
|
|
127
|
+
|
|
128
|
+
this.useTextTrackPolling = !(
|
|
129
|
+
media.textTracks && 'onchange' in media.textTracks
|
|
130
|
+
);
|
|
131
|
+
if (this.useTextTrackPolling) {
|
|
132
|
+
this.pollTrackChange(500);
|
|
133
|
+
} else {
|
|
134
|
+
media.textTracks.addEventListener('change', this.asyncPollTrackChange);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private pollTrackChange(timeout: number) {
|
|
139
|
+
self.clearInterval(this.subtitlePollingInterval);
|
|
140
|
+
this.subtitlePollingInterval = self.setInterval(
|
|
141
|
+
this.onTextTracksChanged,
|
|
142
|
+
timeout,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected onMediaDetaching(
|
|
147
|
+
event: Events.MEDIA_DETACHING,
|
|
148
|
+
data: MediaDetachingData,
|
|
149
|
+
) {
|
|
150
|
+
const media = this.media;
|
|
151
|
+
if (!media) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const transferringMedia = !!data.transferMedia;
|
|
156
|
+
self.clearInterval(this.subtitlePollingInterval);
|
|
157
|
+
if (!this.useTextTrackPolling) {
|
|
158
|
+
media.textTracks.removeEventListener('change', this.asyncPollTrackChange);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.trackId > -1) {
|
|
162
|
+
this.queuedDefaultTrack = this.trackId;
|
|
163
|
+
|
|
164
|
+
// Disable all subtitle tracks before detachment so when reattached only tracks in that content are enabled.
|
|
165
|
+
this.setSubtitleTrack(-1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.media = null;
|
|
169
|
+
if (transferringMedia) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.hls.config.renderTextTracksNatively) {
|
|
174
|
+
this.tracksInGroup.forEach((track) => {
|
|
175
|
+
if (track.trackNode) {
|
|
176
|
+
track.trackNode.remove();
|
|
177
|
+
track.trackNode = undefined;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
protected onManifestLoading(): void {
|
|
184
|
+
this.tracks = [];
|
|
185
|
+
this.groupIds = null;
|
|
186
|
+
this.tracksInGroup = [];
|
|
187
|
+
this.trackId = -1;
|
|
188
|
+
this.currentTrack = null;
|
|
189
|
+
this.selectDefaultTrack = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Fired whenever a new manifest is loaded.
|
|
193
|
+
protected onManifestParsed(
|
|
194
|
+
event: Events.MANIFEST_PARSED,
|
|
195
|
+
data: ManifestParsedData,
|
|
196
|
+
): void {
|
|
197
|
+
this.tracks = data.subtitleTracks;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
protected onSubtitleTrackLoaded(
|
|
201
|
+
event: Events.SUBTITLE_TRACK_LOADED,
|
|
202
|
+
data: TrackLoadedData,
|
|
203
|
+
): void {
|
|
204
|
+
const { id, groupId, details } = data;
|
|
205
|
+
const trackInActiveGroup = this.tracksInGroup[id];
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
!trackInActiveGroup ||
|
|
209
|
+
trackInActiveGroup.groupId !== (groupId as string | undefined)
|
|
210
|
+
) {
|
|
211
|
+
this.warn(
|
|
212
|
+
`Subtitle track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup?.groupId}`,
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const curDetails = trackInActiveGroup.details;
|
|
218
|
+
trackInActiveGroup.details = data.details;
|
|
219
|
+
this.log(
|
|
220
|
+
`Subtitle track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (id === this.trackId) {
|
|
224
|
+
this.playlistLoaded(id, data, curDetails);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
protected onLevelLoading(
|
|
229
|
+
event: Events.LEVEL_LOADING,
|
|
230
|
+
data: LevelLoadingData,
|
|
231
|
+
): void {
|
|
232
|
+
this.switchLevel(data.level);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
protected onLevelSwitching(
|
|
236
|
+
event: Events.LEVEL_SWITCHING,
|
|
237
|
+
data: LevelSwitchingData,
|
|
238
|
+
): void {
|
|
239
|
+
this.switchLevel(data.level);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private switchLevel(levelIndex: number) {
|
|
243
|
+
const levelInfo = this.hls.levels[levelIndex];
|
|
244
|
+
if (!levelInfo) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const subtitleGroups = levelInfo.subtitleGroups || null;
|
|
248
|
+
const currentGroups = this.groupIds;
|
|
249
|
+
let currentTrack = this.currentTrack;
|
|
250
|
+
if (
|
|
251
|
+
!subtitleGroups ||
|
|
252
|
+
currentGroups?.length !== subtitleGroups?.length ||
|
|
253
|
+
subtitleGroups?.some((groupId) => currentGroups?.indexOf(groupId) === -1)
|
|
254
|
+
) {
|
|
255
|
+
this.groupIds = subtitleGroups;
|
|
256
|
+
const subtitleTracks: MediaPlaylist[] = [];
|
|
257
|
+
this.tracks.forEach((track) => {
|
|
258
|
+
if (
|
|
259
|
+
track.textCodec === IMSC1_CODEC
|
|
260
|
+
? this.hls.config.enableIMSC1
|
|
261
|
+
: this.hls.config.enableWebVTT
|
|
262
|
+
) {
|
|
263
|
+
if (!subtitleGroups || subtitleGroups.includes(track.groupId)) {
|
|
264
|
+
// track.id should match hls.subtitleTracks index
|
|
265
|
+
track.id = subtitleTracks.length;
|
|
266
|
+
subtitleTracks.push(track);
|
|
267
|
+
} else if (track.trackNode) {
|
|
268
|
+
track.trackNode.remove();
|
|
269
|
+
track.trackNode = undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
if (subtitleTracks.length === this.tracksInGroup.length) {
|
|
274
|
+
let diff = false;
|
|
275
|
+
for (let i = 0; i < subtitleTracks.length; i++) {
|
|
276
|
+
if (subtitleTracks[i] !== this.tracksInGroup[i]) {
|
|
277
|
+
diff = true;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!diff) {
|
|
282
|
+
// Do not dispatch SUBTITLE_TRACKS_UPDATED if there are no changes
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (subtitleTracks.length) {
|
|
287
|
+
// Disable selectDefaultTrack if there are no default tracks
|
|
288
|
+
if (
|
|
289
|
+
this.selectDefaultTrack &&
|
|
290
|
+
!subtitleTracks.some((track) => track.default)
|
|
291
|
+
) {
|
|
292
|
+
this.selectDefaultTrack = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
this.tracksInGroup = subtitleTracks;
|
|
296
|
+
this.trackId = -1;
|
|
297
|
+
this.currentTrack = null;
|
|
298
|
+
|
|
299
|
+
// Find preferred track
|
|
300
|
+
const subtitlePreference = this.hls.config.subtitlePreference;
|
|
301
|
+
if (!currentTrack && subtitlePreference) {
|
|
302
|
+
this.selectDefaultTrack = false;
|
|
303
|
+
const groupIndex = findMatchingOption(
|
|
304
|
+
subtitlePreference,
|
|
305
|
+
subtitleTracks,
|
|
306
|
+
);
|
|
307
|
+
if (groupIndex > -1) {
|
|
308
|
+
currentTrack = subtitleTracks[groupIndex];
|
|
309
|
+
} else {
|
|
310
|
+
const allIndex = findMatchingOption(subtitlePreference, this.tracks);
|
|
311
|
+
currentTrack = this.tracks[allIndex];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Select initial track
|
|
316
|
+
let trackId = this.findTrackId(currentTrack);
|
|
317
|
+
if (trackId === -1 && currentTrack) {
|
|
318
|
+
trackId = this.findTrackId(null);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Dispatch events and load track if needed
|
|
322
|
+
const subtitleTracksUpdated: SubtitleTracksUpdatedData = {
|
|
323
|
+
subtitleTracks,
|
|
324
|
+
};
|
|
325
|
+
this.log(
|
|
326
|
+
`Updating subtitle tracks, ${
|
|
327
|
+
subtitleTracks.length
|
|
328
|
+
} track(s) found in "${subtitleGroups?.join(',')}" group-id`,
|
|
329
|
+
);
|
|
330
|
+
this.hls.trigger(Events.SUBTITLE_TRACKS_UPDATED, subtitleTracksUpdated);
|
|
331
|
+
this.setSubtitleTrack(trackId);
|
|
332
|
+
this.createTracksInGroup();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private findTrackId(currentTrack: MediaPlaylist | null): number {
|
|
337
|
+
const tracks = this.tracksInGroup;
|
|
338
|
+
const selectDefault = this.selectDefaultTrack;
|
|
339
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
340
|
+
const track = tracks[i];
|
|
341
|
+
if (
|
|
342
|
+
(selectDefault && !track.default) ||
|
|
343
|
+
(!selectDefault && !currentTrack)
|
|
344
|
+
) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!currentTrack || matchesOption(track, currentTrack)) {
|
|
348
|
+
return i;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (currentTrack) {
|
|
352
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
353
|
+
const track = tracks[i];
|
|
354
|
+
if (
|
|
355
|
+
mediaAttributesIdentical(currentTrack.attrs, track.attrs, [
|
|
356
|
+
'LANGUAGE',
|
|
357
|
+
'ASSOC-LANGUAGE',
|
|
358
|
+
'CHARACTERISTICS',
|
|
359
|
+
])
|
|
360
|
+
) {
|
|
361
|
+
return i;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
365
|
+
const track = tracks[i];
|
|
366
|
+
if (
|
|
367
|
+
mediaAttributesIdentical(currentTrack.attrs, track.attrs, [
|
|
368
|
+
'LANGUAGE',
|
|
369
|
+
])
|
|
370
|
+
) {
|
|
371
|
+
return i;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return -1;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
protected onError(event: Events.ERROR, data: ErrorData): void {
|
|
379
|
+
if (data.fatal || !data.context) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (
|
|
384
|
+
data.context.type === PlaylistContextType.SUBTITLE_TRACK &&
|
|
385
|
+
data.context.id === this.trackId &&
|
|
386
|
+
(!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)
|
|
387
|
+
) {
|
|
388
|
+
this.checkRetry(data);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
get allSubtitleTracks(): MediaPlaylist[] {
|
|
393
|
+
return this.tracks;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** get alternate subtitle tracks list from playlist **/
|
|
397
|
+
get subtitleTracks(): MediaPlaylist[] {
|
|
398
|
+
return this.tracksInGroup;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** get/set index of the selected subtitle track (based on index in subtitle track lists) **/
|
|
402
|
+
get subtitleTrack(): number {
|
|
403
|
+
return this.trackId;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
set subtitleTrack(newId: number) {
|
|
407
|
+
this.selectDefaultTrack = false;
|
|
408
|
+
this.setSubtitleTrack(newId, true);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
public setSubtitleOption(
|
|
412
|
+
subtitleOption: MediaPlaylist | SubtitleSelectionOption | undefined,
|
|
413
|
+
): MediaPlaylist | null {
|
|
414
|
+
this.hls.config.subtitlePreference = subtitleOption;
|
|
415
|
+
if (subtitleOption) {
|
|
416
|
+
if (subtitleOption.id === -1) {
|
|
417
|
+
this.setSubtitleTrack(-1, true);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const allSubtitleTracks = this.allSubtitleTracks;
|
|
421
|
+
this.selectDefaultTrack = false;
|
|
422
|
+
if (allSubtitleTracks.length) {
|
|
423
|
+
// First see if current option matches (no switch op)
|
|
424
|
+
const currentTrack = this.currentTrack;
|
|
425
|
+
if (currentTrack && matchesOption(subtitleOption, currentTrack)) {
|
|
426
|
+
return currentTrack;
|
|
427
|
+
}
|
|
428
|
+
// Find option in current group
|
|
429
|
+
const groupIndex = findMatchingOption(
|
|
430
|
+
subtitleOption,
|
|
431
|
+
this.tracksInGroup,
|
|
432
|
+
);
|
|
433
|
+
if (groupIndex > -1) {
|
|
434
|
+
const track = this.tracksInGroup[groupIndex];
|
|
435
|
+
this.setSubtitleTrack(groupIndex, true);
|
|
436
|
+
return track;
|
|
437
|
+
} else if (currentTrack) {
|
|
438
|
+
// If this is not the initial selection return null
|
|
439
|
+
// option should have matched one in active group
|
|
440
|
+
return null;
|
|
441
|
+
} else {
|
|
442
|
+
// Find the option in all tracks for initial selection
|
|
443
|
+
const allIndex = findMatchingOption(
|
|
444
|
+
subtitleOption,
|
|
445
|
+
allSubtitleTracks,
|
|
446
|
+
);
|
|
447
|
+
if (allIndex > -1) {
|
|
448
|
+
return allSubtitleTracks[allIndex];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
|
|
457
|
+
super.loadPlaylist();
|
|
458
|
+
if (this.shouldLoadPlaylist(this.currentTrack)) {
|
|
459
|
+
this.scheduleLoading(this.currentTrack, hlsUrlParameters);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
protected loadingPlaylist(
|
|
464
|
+
currentTrack: MediaPlaylist,
|
|
465
|
+
hlsUrlParameters: HlsUrlParameters | undefined,
|
|
466
|
+
) {
|
|
467
|
+
super.loadingPlaylist(currentTrack, hlsUrlParameters);
|
|
468
|
+
const id = currentTrack.id;
|
|
469
|
+
const groupId = currentTrack.groupId as string;
|
|
470
|
+
const url = this.getUrlWithDirectives(currentTrack.url, hlsUrlParameters);
|
|
471
|
+
const details = currentTrack.details;
|
|
472
|
+
const age = details?.age;
|
|
473
|
+
this.log(
|
|
474
|
+
`Loading subtitle ${id} "${currentTrack.name}" lang:${currentTrack.lang} group:${groupId}${
|
|
475
|
+
hlsUrlParameters?.msn !== undefined
|
|
476
|
+
? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part
|
|
477
|
+
: ''
|
|
478
|
+
}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`,
|
|
479
|
+
);
|
|
480
|
+
this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, {
|
|
481
|
+
url,
|
|
482
|
+
id,
|
|
483
|
+
groupId,
|
|
484
|
+
deliveryDirectives: hlsUrlParameters || null,
|
|
485
|
+
track: currentTrack,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Disables the old subtitleTrack and sets current mode on the next subtitleTrack.
|
|
491
|
+
* This operates on the DOM textTracks.
|
|
492
|
+
* A value of -1 will disable all subtitle tracks.
|
|
493
|
+
*/
|
|
494
|
+
private toggleTrackModes(): void {
|
|
495
|
+
if (!this.media || !this.hls.config.renderTextTracksNatively) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const nextTrack = this.currentTrack;
|
|
500
|
+
this.tracksInGroup.forEach((track) => {
|
|
501
|
+
if (track.trackNode) {
|
|
502
|
+
const mode =
|
|
503
|
+
track === nextTrack
|
|
504
|
+
? this._subtitleDisplay
|
|
505
|
+
? 'showing'
|
|
506
|
+
: 'hidden'
|
|
507
|
+
: 'disabled';
|
|
508
|
+
const textTrack = track.trackNode.track;
|
|
509
|
+
if (textTrack.mode !== mode) {
|
|
510
|
+
textTrack.mode = mode;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* This method is responsible for validating the subtitle index and periodically reloading if live.
|
|
518
|
+
* Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track.
|
|
519
|
+
*/
|
|
520
|
+
private setSubtitleTrack(newId: number, toggleModes: boolean = false): void {
|
|
521
|
+
const tracks = this.tracksInGroup;
|
|
522
|
+
|
|
523
|
+
// setting this.subtitleTrack will trigger internal logic
|
|
524
|
+
// if media has not been attached yet, it will fail
|
|
525
|
+
// we keep a reference to the default track id
|
|
526
|
+
// and we'll set subtitleTrack when onMediaAttached is triggered
|
|
527
|
+
if (!this.media) {
|
|
528
|
+
this.queuedDefaultTrack = newId;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// exit if track id as already set or invalid
|
|
532
|
+
if (newId < -1 || newId >= tracks.length || !Number.isFinite(newId)) {
|
|
533
|
+
this.warn(`Invalid subtitle track id: ${newId}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const lastTrack = this.currentTrack;
|
|
538
|
+
const track: MediaPlaylist | null = tracks[newId] || null;
|
|
539
|
+
this.trackId = newId;
|
|
540
|
+
this.currentTrack = track;
|
|
541
|
+
if (toggleModes) {
|
|
542
|
+
this.toggleTrackModes();
|
|
543
|
+
}
|
|
544
|
+
if (!track) {
|
|
545
|
+
if (lastTrack) {
|
|
546
|
+
// switch to -1
|
|
547
|
+
this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { id: newId });
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const trackLoaded = !!track.details && !track.details.live;
|
|
552
|
+
if (track === lastTrack && trackLoaded) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this.log(
|
|
556
|
+
`Switching to subtitle-track ${newId}` +
|
|
557
|
+
(track
|
|
558
|
+
? ` "${track.name}" lang:${track.lang} group:${track.groupId}`
|
|
559
|
+
: ''),
|
|
560
|
+
);
|
|
561
|
+
const { id, groupId = '', name, type, url } = track;
|
|
562
|
+
this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, {
|
|
563
|
+
id,
|
|
564
|
+
groupId,
|
|
565
|
+
name,
|
|
566
|
+
type,
|
|
567
|
+
url,
|
|
568
|
+
});
|
|
569
|
+
const hlsUrlParameters = this.switchParams(
|
|
570
|
+
track.url,
|
|
571
|
+
lastTrack?.details,
|
|
572
|
+
track.details,
|
|
573
|
+
);
|
|
574
|
+
this.loadPlaylist(hlsUrlParameters);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private onTextTracksChanged = () => {
|
|
578
|
+
if (!this.useTextTrackPolling) {
|
|
579
|
+
self.clearInterval(this.subtitlePollingInterval);
|
|
580
|
+
}
|
|
581
|
+
// Media is undefined when switching streams via loadSource()
|
|
582
|
+
if (!this.media || !this.hls.config.renderTextTracksNatively) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
let trackId = -1;
|
|
586
|
+
let found = false;
|
|
587
|
+
// Prefer previously selected track
|
|
588
|
+
if (this.currentTrack) {
|
|
589
|
+
const mode = this.currentTrack.trackNode?.track.mode;
|
|
590
|
+
if (mode === 'showing') {
|
|
591
|
+
trackId = this.trackId;
|
|
592
|
+
found = true;
|
|
593
|
+
} else if (mode === 'hidden') {
|
|
594
|
+
trackId = this.trackId;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (!found) {
|
|
598
|
+
for (let i = 0; i < this.tracksInGroup.length; i++) {
|
|
599
|
+
const mode = this.tracksInGroup[i].trackNode?.track.mode;
|
|
600
|
+
if (mode === 'showing') {
|
|
601
|
+
trackId = i;
|
|
602
|
+
break;
|
|
603
|
+
} else if (trackId < 0 && mode === 'hidden') {
|
|
604
|
+
// If there is no showing track, we can use the hidden track
|
|
605
|
+
trackId = i;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (trackId > -1) {
|
|
610
|
+
this._subtitleDisplay =
|
|
611
|
+
this.tracksInGroup[trackId].trackNode?.track.mode === 'showing';
|
|
612
|
+
}
|
|
613
|
+
this.setSubtitleTrack(trackId, true);
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export default SubtitleTrackController;
|