@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.
Files changed (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. 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;