@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,1099 @@
1
+ import BaseStreamController, { State } from './base-stream-controller';
2
+ import { findNearestWithCC } from './fragment-finders';
3
+ import { FragmentState } from './fragment-tracker';
4
+ import ChunkCache from '../demux/chunk-cache';
5
+ import TransmuxerInterface from '../demux/transmuxer-interface';
6
+ import { ErrorDetails } from '../errors';
7
+ import { Events } from '../events';
8
+ import { ElementaryStreamTypes, isMediaFragment } from '../loader/fragment';
9
+ import { Level } from '../types/level';
10
+ import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
11
+ import { ChunkMetadata } from '../types/transmuxer';
12
+ import { alignStream } from '../utils/discontinuities';
13
+ import {
14
+ audioMatchPredicate,
15
+ matchesOption,
16
+ useAlternateAudio,
17
+ } from '../utils/rendition-helper';
18
+ import type { FragmentTracker } from './fragment-tracker';
19
+ import type { Bufferable } from '../hls';
20
+ import type Hls from '../hls';
21
+ import type { Fragment, MediaFragment, Part } from '../loader/fragment';
22
+ import type KeyLoader from '../loader/key-loader';
23
+ import type { LevelDetails } from '../loader/level-details';
24
+ import type { NetworkComponentAPI } from '../types/component-api';
25
+ import type {
26
+ AudioTracksUpdatedData,
27
+ AudioTrackSwitchingData,
28
+ BufferAppendingData,
29
+ BufferCodecsData,
30
+ BufferCreatedData,
31
+ BufferFlushedData,
32
+ BufferFlushingData,
33
+ ErrorData,
34
+ FragBufferedData,
35
+ FragLoadedData,
36
+ FragLoadingData,
37
+ FragParsingMetadataData,
38
+ FragParsingUserdataData,
39
+ InitPTSFoundData,
40
+ LevelLoadedData,
41
+ MediaDetachingData,
42
+ TrackLoadedData,
43
+ } from '../types/events';
44
+ import type { MediaPlaylist } from '../types/media-playlist';
45
+ import type { TrackSet } from '../types/track';
46
+ import type { TransmuxerResult } from '../types/transmuxer';
47
+
48
+ const TICK_INTERVAL = 100; // how often to tick in ms
49
+
50
+ type WaitingForPTSData = {
51
+ frag: MediaFragment;
52
+ part: Part | null;
53
+ cache: ChunkCache;
54
+ complete: boolean;
55
+ };
56
+
57
+ class AudioStreamController
58
+ extends BaseStreamController
59
+ implements NetworkComponentAPI
60
+ {
61
+ private mainAnchor: MediaFragment | null = null;
62
+ private mainFragLoading: FragLoadingData | null = null;
63
+ private audioOnly: boolean = false;
64
+ private bufferedTrack: MediaPlaylist | null = null;
65
+ private switchingTrack: AudioTrackSwitchingData | null = null;
66
+ private trackId: number = -1;
67
+ private nextTrackId: number = -1;
68
+ private waitingData: WaitingForPTSData | null = null;
69
+ private mainDetails: LevelDetails | null = null;
70
+ private flushing: boolean = false;
71
+ private bufferFlushed: boolean = false;
72
+ private cachedTrackLoadedData: TrackLoadedData | null = null;
73
+
74
+ constructor(
75
+ hls: Hls,
76
+ fragmentTracker: FragmentTracker,
77
+ keyLoader: KeyLoader,
78
+ ) {
79
+ super(
80
+ hls,
81
+ fragmentTracker,
82
+ keyLoader,
83
+ 'audio-stream-controller',
84
+ PlaylistLevelType.AUDIO,
85
+ );
86
+ this.registerListeners();
87
+ }
88
+
89
+ protected onHandlerDestroying() {
90
+ this.unregisterListeners();
91
+ super.onHandlerDestroying();
92
+ this.resetItem();
93
+ }
94
+
95
+ private resetItem() {
96
+ this.mainDetails =
97
+ this.mainAnchor =
98
+ this.mainFragLoading =
99
+ this.bufferedTrack =
100
+ this.switchingTrack =
101
+ this.waitingData =
102
+ this.cachedTrackLoadedData =
103
+ null;
104
+ }
105
+
106
+ protected registerListeners() {
107
+ super.registerListeners();
108
+ const { hls } = this;
109
+ hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
110
+ hls.on(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this);
111
+ hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
112
+ hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
113
+ hls.on(Events.BUFFER_RESET, this.onBufferReset, this);
114
+ hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this);
115
+ hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
116
+ hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
117
+ hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
118
+ hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
119
+ hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
120
+ }
121
+
122
+ protected unregisterListeners() {
123
+ const { hls } = this;
124
+ if (!hls) {
125
+ return;
126
+ }
127
+ super.unregisterListeners();
128
+ hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
129
+ hls.off(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this);
130
+ hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this);
131
+ hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
132
+ hls.off(Events.BUFFER_RESET, this.onBufferReset, this);
133
+ hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this);
134
+ hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
135
+ hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
136
+ hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
137
+ hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
138
+ hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
139
+ }
140
+
141
+ // INIT_PTS_FOUND is triggered when the video track parsed in the stream-controller has a new PTS value
142
+ onInitPtsFound(
143
+ event: Events.INIT_PTS_FOUND,
144
+ { frag, id, initPTS, timescale, trackId }: InitPTSFoundData,
145
+ ) {
146
+ // Always update the new INIT PTS
147
+ // Can change due level switch
148
+ if (id === PlaylistLevelType.MAIN) {
149
+ const cc = frag.cc;
150
+ const inFlightFrag = this.fragCurrent;
151
+ this.initPTS[cc] = { baseTime: initPTS, timescale, trackId };
152
+ this.log(
153
+ `InitPTS for cc: ${cc} found from main: ${initPTS / timescale} (${initPTS}/${timescale}) trackId: ${trackId}`,
154
+ );
155
+ this.mainAnchor = frag;
156
+ // If we are waiting, tick immediately to unblock audio fragment transmuxing
157
+ if (this.state === State.WAITING_INIT_PTS) {
158
+ const waitingData = this.waitingData;
159
+ if (
160
+ (!waitingData && !this.loadingParts) ||
161
+ (waitingData && waitingData.frag.cc !== cc)
162
+ ) {
163
+ this.syncWithAnchor(frag, waitingData?.frag);
164
+ }
165
+ } else if (
166
+ !this.hls.hasEnoughToStart &&
167
+ inFlightFrag &&
168
+ inFlightFrag.cc !== cc
169
+ ) {
170
+ inFlightFrag.abortRequests();
171
+ this.syncWithAnchor(frag, inFlightFrag);
172
+ } else if (this.state === State.IDLE) {
173
+ this.tick();
174
+ }
175
+ }
176
+ }
177
+
178
+ protected getLoadPosition(): number {
179
+ if (
180
+ !this.startFragRequested &&
181
+ this.nextLoadPosition >= 0 &&
182
+ this.switchingTrack?.flushImmediate !== false
183
+ ) {
184
+ return this.nextLoadPosition;
185
+ }
186
+ return super.getLoadPosition();
187
+ }
188
+
189
+ private syncWithAnchor(
190
+ mainAnchor: MediaFragment,
191
+ waitingToAppend: Fragment | undefined,
192
+ ) {
193
+ // Drop waiting fragment if videoTrackCC has changed since waitingFragment was set and initPTS was not found
194
+ const mainFragLoading = this.mainFragLoading?.frag || null;
195
+ if (waitingToAppend) {
196
+ if (mainFragLoading?.cc === waitingToAppend.cc) {
197
+ // Wait for loading frag to complete and INIT_PTS_FOUND
198
+ return;
199
+ }
200
+ }
201
+ const targetDiscontinuity = (mainFragLoading || mainAnchor).cc;
202
+ const trackDetails = this.getLevelDetails();
203
+ const pos = this.getLoadPosition();
204
+ const syncFrag = findNearestWithCC(trackDetails, targetDiscontinuity, pos);
205
+ // Only stop waiting for audioFrag.cc if an audio segment of the same discontinuity domain (cc) is found
206
+ if (syncFrag) {
207
+ this.log(`Syncing with main frag at ${syncFrag.start} cc ${syncFrag.cc}`);
208
+ this.startFragRequested = false;
209
+ this.nextLoadPosition = syncFrag.start;
210
+ this.resetLoadingState();
211
+ if (this.state === State.IDLE) {
212
+ this.doTickIdle();
213
+ }
214
+ }
215
+ }
216
+
217
+ startLoad(startPosition: number, skipSeekToStartPosition?: boolean) {
218
+ if (!this.levels) {
219
+ this.startPosition = startPosition;
220
+ this.state = State.STOPPED;
221
+ return;
222
+ }
223
+ const lastCurrentTime = this.lastCurrentTime;
224
+ this.stopLoad();
225
+ this.setInterval(TICK_INTERVAL);
226
+ if (lastCurrentTime > 0 && startPosition === -1) {
227
+ this.log(
228
+ `Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(
229
+ 3,
230
+ )}`,
231
+ );
232
+ startPosition = lastCurrentTime;
233
+ this.state = State.IDLE;
234
+ } else {
235
+ this.state = State.WAITING_TRACK;
236
+ }
237
+ this.nextLoadPosition = this.lastCurrentTime =
238
+ startPosition + this.timelineOffset;
239
+ this.startPosition = skipSeekToStartPosition ? -1 : startPosition;
240
+ this.tick();
241
+ }
242
+
243
+ doTick() {
244
+ switch (this.state) {
245
+ case State.IDLE:
246
+ this.doTickIdle();
247
+ break;
248
+ case State.WAITING_TRACK: {
249
+ const { levels, trackId } = this;
250
+ const currenTrack = levels?.[trackId];
251
+ const details = currenTrack?.details;
252
+ if (details && !this.waitForLive(currenTrack)) {
253
+ if (this.waitForCdnTuneIn(details)) {
254
+ break;
255
+ }
256
+ this.state = State.WAITING_INIT_PTS;
257
+ }
258
+ break;
259
+ }
260
+ case State.FRAG_LOADING_WAITING_RETRY: {
261
+ this.checkRetryDate();
262
+ break;
263
+ }
264
+ case State.WAITING_INIT_PTS: {
265
+ // Ensure we don't get stuck in the WAITING_INIT_PTS state if the waiting frag CC doesn't match any initPTS
266
+ const waitingData = this.waitingData;
267
+ if (waitingData) {
268
+ const { frag, part, cache, complete } = waitingData;
269
+ const mainAnchor = this.mainAnchor;
270
+ if (this.initPTS[frag.cc] !== undefined) {
271
+ this.waitingData = null;
272
+ this.state = State.FRAG_LOADING;
273
+ const payload = cache.flush().buffer;
274
+ const data: FragLoadedData = {
275
+ frag,
276
+ part,
277
+ payload,
278
+ networkDetails: null,
279
+ };
280
+ this._handleFragmentLoadProgress(data);
281
+ if (complete) {
282
+ super._handleFragmentLoadComplete(data);
283
+ }
284
+ } else if (mainAnchor && mainAnchor.cc !== waitingData.frag.cc) {
285
+ this.syncWithAnchor(mainAnchor, waitingData.frag);
286
+ }
287
+ } else {
288
+ this.state = State.IDLE;
289
+ }
290
+ }
291
+ }
292
+
293
+ this.onTickEnd();
294
+ }
295
+
296
+ protected resetLoadingState() {
297
+ const waitingData = this.waitingData;
298
+ if (waitingData) {
299
+ this.fragmentTracker.removeFragment(waitingData.frag);
300
+ this.waitingData = null;
301
+ }
302
+ super.resetLoadingState();
303
+ }
304
+
305
+ protected onTickEnd() {
306
+ const { media } = this;
307
+ if (!media?.readyState) {
308
+ // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0)
309
+ return;
310
+ }
311
+
312
+ this.lastCurrentTime = media.currentTime;
313
+ this.checkFragmentChanged();
314
+ }
315
+
316
+ private doTickIdle() {
317
+ const { hls, levels, media, trackId } = this;
318
+ const config = hls.config;
319
+
320
+ // 1. if buffering is suspended
321
+ // 2. if video not attached AND
322
+ // start fragment already requested OR start frag prefetch not enabled
323
+ // 3. if tracks or track not loaded and selected
324
+ // then exit loop
325
+ // => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop
326
+ if (
327
+ !this.buffering ||
328
+ (!media &&
329
+ !this.primaryPrefetch &&
330
+ (this.startFragRequested || !config.startFragPrefetch)) ||
331
+ !levels?.[trackId]
332
+ ) {
333
+ return;
334
+ }
335
+
336
+ const levelInfo = levels[trackId];
337
+
338
+ const trackDetails = levelInfo.details;
339
+ if (
340
+ !trackDetails ||
341
+ this.waitForLive(levelInfo) ||
342
+ this.waitForCdnTuneIn(trackDetails)
343
+ ) {
344
+ this.state = State.WAITING_TRACK;
345
+ this.startFragRequested = false;
346
+ return;
347
+ }
348
+
349
+ const bufferable = this.mediaBuffer ? this.mediaBuffer : this.media;
350
+ if (this.bufferFlushed && bufferable) {
351
+ this.bufferFlushed = false;
352
+ this.afterBufferFlushed(bufferable, ElementaryStreamTypes.AUDIO);
353
+ }
354
+
355
+ const bufferInfo = this.getFwdBufferInfo(
356
+ bufferable,
357
+ PlaylistLevelType.AUDIO,
358
+ );
359
+ if (bufferInfo === null) {
360
+ return;
361
+ }
362
+
363
+ if (!this.switchingTrack && this._streamEnded(bufferInfo, trackDetails)) {
364
+ hls.trigger(Events.BUFFER_EOS, { type: 'audio' });
365
+ this.state = State.ENDED;
366
+ return;
367
+ }
368
+
369
+ const bufferLen = bufferInfo.len;
370
+ const maxBufLen = hls.maxBufferLength;
371
+
372
+ const fragments = trackDetails.fragments;
373
+ const start = fragments[0].start;
374
+ const loadPosition = this.getLoadPosition();
375
+ const targetBufferTime =
376
+ this.flushing ||
377
+ (this.switchingTrack && !this.switchingTrack.flushImmediate)
378
+ ? loadPosition
379
+ : bufferInfo.end;
380
+
381
+ if (this.switchingTrack && media) {
382
+ const pos = loadPosition;
383
+ // if currentTime (pos) is less than alt audio playlist start time, it means that alt audio is ahead of currentTime
384
+ if (trackDetails.PTSKnown && pos < start) {
385
+ // if everything is buffered from pos to start or if audio buffer upfront, let's seek to start
386
+ if (bufferInfo.end > start || bufferInfo.nextStart) {
387
+ this.log(
388
+ 'Alt audio track ahead of main track, seek to start of alt audio track',
389
+ );
390
+ media.currentTime = start + 0.05;
391
+ }
392
+ }
393
+ }
394
+
395
+ // if buffer length is less than maxBufLen, or near the end, find a fragment to load
396
+ if (
397
+ bufferLen >= maxBufLen &&
398
+ !this.switchingTrack &&
399
+ targetBufferTime < fragments[fragments.length - 1].start
400
+ ) {
401
+ return;
402
+ }
403
+
404
+ let frag = this.getNextFragment(targetBufferTime, trackDetails);
405
+ // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags
406
+ if (frag && this.isLoopLoading(frag, targetBufferTime)) {
407
+ frag = this.getNextFragmentLoopLoading(
408
+ frag,
409
+ trackDetails,
410
+ bufferInfo,
411
+ PlaylistLevelType.MAIN,
412
+ maxBufLen,
413
+ );
414
+ }
415
+ if (!frag) {
416
+ this.bufferFlushed = true;
417
+ return;
418
+ }
419
+
420
+ // Request audio segments up to one fragment ahead of main stream-controller
421
+ let mainFragLoading = this.mainFragLoading?.frag || null;
422
+ if (
423
+ !this.audioOnly &&
424
+ this.startFragRequested &&
425
+ mainFragLoading &&
426
+ isMediaFragment(frag) &&
427
+ !frag.endList &&
428
+ (!trackDetails.live ||
429
+ (!this.loadingParts && targetBufferTime < this.hls.liveSyncPosition!))
430
+ ) {
431
+ if (this.fragmentTracker.getState(mainFragLoading) === FragmentState.OK) {
432
+ this.mainFragLoading = mainFragLoading = null;
433
+ }
434
+ if (mainFragLoading && isMediaFragment(mainFragLoading)) {
435
+ if (frag.start > mainFragLoading.end) {
436
+ // Get buffered frag at target position from tracker (loaded out of sequence)
437
+ const mainFragAtPos = this.fragmentTracker.getFragAtPos(
438
+ targetBufferTime,
439
+ PlaylistLevelType.MAIN,
440
+ );
441
+ if (mainFragAtPos && mainFragAtPos.end > mainFragLoading.end) {
442
+ mainFragLoading = mainFragAtPos;
443
+ this.mainFragLoading = {
444
+ frag: mainFragAtPos,
445
+ targetBufferTime: null,
446
+ };
447
+ }
448
+ }
449
+ const atBufferSyncLimit = frag.start > mainFragLoading.end;
450
+ if (atBufferSyncLimit) {
451
+ return;
452
+ }
453
+ }
454
+ }
455
+
456
+ this.loadFragment(frag, levelInfo, targetBufferTime);
457
+ }
458
+
459
+ protected onMediaDetaching(
460
+ event: Events.MEDIA_DETACHING,
461
+ data: MediaDetachingData,
462
+ ) {
463
+ this.bufferFlushed = this.flushing = false;
464
+ super.onMediaDetaching(event, data);
465
+ }
466
+
467
+ private onAudioTracksUpdated(
468
+ event: Events.AUDIO_TRACKS_UPDATED,
469
+ { audioTracks }: AudioTracksUpdatedData,
470
+ ) {
471
+ // Reset tranxmuxer is essential for large context switches (Content Steering)
472
+ this.resetTransmuxer();
473
+ this.levels = audioTracks.map((mediaPlaylist) => new Level(mediaPlaylist));
474
+ }
475
+
476
+ private onAudioTrackSwitching(
477
+ event: Events.AUDIO_TRACK_SWITCHING,
478
+ data: AudioTrackSwitchingData,
479
+ ) {
480
+ // if any URL found on new audio track, it is an alternate audio track
481
+ const altAudio = !!data.url;
482
+ this.trackId = data.id;
483
+ const { fragCurrent } = this;
484
+
485
+ if (fragCurrent) {
486
+ fragCurrent.abortRequests();
487
+ this.removeUnbufferedFrags(fragCurrent.start);
488
+ }
489
+ this.resetLoadingState();
490
+
491
+ // should we switch tracks ?
492
+ if (altAudio) {
493
+ this.switchingTrack = data;
494
+ // main audio track are handled by stream-controller, just do something if switching to alt audio track
495
+ if (!data.flushImmediate) {
496
+ this.nextTrackId = data.id;
497
+ this.nextLevelSwitch();
498
+ }
499
+ this.flushAudioIfNeeded(data);
500
+ if (this.state !== State.STOPPED) {
501
+ // switching to audio track, start timer if not already started
502
+ this.setInterval(TICK_INTERVAL);
503
+ this.state = State.IDLE;
504
+ this.tick();
505
+ }
506
+ } else {
507
+ // destroy useless transmuxer when switching audio to main
508
+ this.resetTransmuxer();
509
+ this.switchingTrack = null;
510
+ this.bufferedTrack = data;
511
+ this.clearInterval();
512
+ }
513
+ }
514
+
515
+ protected onManifestLoading() {
516
+ super.onManifestLoading();
517
+ this.bufferFlushed = this.flushing = this.audioOnly = false;
518
+ this.resetItem();
519
+ this.trackId = -1;
520
+ }
521
+
522
+ private onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
523
+ this.mainDetails = data.details;
524
+ const cachedTrackLoadedData = this.cachedTrackLoadedData;
525
+ if (cachedTrackLoadedData) {
526
+ this.cachedTrackLoadedData = null;
527
+ this.onAudioTrackLoaded(Events.AUDIO_TRACK_LOADED, cachedTrackLoadedData);
528
+ }
529
+ }
530
+
531
+ private onAudioTrackLoaded(
532
+ event: Events.AUDIO_TRACK_LOADED,
533
+ data: TrackLoadedData,
534
+ ) {
535
+ const { levels } = this;
536
+ const { details: newDetails, id: trackId, groupId, track } = data;
537
+ if (!levels) {
538
+ this.warn(
539
+ `Audio tracks reset while loading track ${trackId} "${track.name}" of "${groupId}"`,
540
+ );
541
+ return;
542
+ }
543
+ const mainDetails = this.mainDetails;
544
+ if (
545
+ !mainDetails ||
546
+ newDetails.endCC > mainDetails.endCC ||
547
+ mainDetails.expired
548
+ ) {
549
+ this.cachedTrackLoadedData = data;
550
+ if (this.state !== State.STOPPED) {
551
+ this.state = State.WAITING_TRACK;
552
+ }
553
+ return;
554
+ }
555
+ this.cachedTrackLoadedData = null;
556
+ this.log(
557
+ `Audio track ${trackId} "${track.name}" of "${groupId}" loaded [${newDetails.startSN},${
558
+ newDetails.endSN
559
+ }]${
560
+ newDetails.lastPartSn
561
+ ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]`
562
+ : ''
563
+ },duration:${newDetails.totalduration}`,
564
+ );
565
+
566
+ const trackLevel = levels[trackId];
567
+ let sliding = 0;
568
+ if (newDetails.live || trackLevel.details?.live) {
569
+ this.checkLiveUpdate(newDetails);
570
+ if (newDetails.deltaUpdateFailed) {
571
+ return;
572
+ }
573
+
574
+ if (trackLevel.details) {
575
+ sliding = this.alignPlaylists(
576
+ newDetails,
577
+ trackLevel.details,
578
+ this.levelLastLoaded?.details,
579
+ );
580
+ }
581
+ if (!newDetails.alignedSliding) {
582
+ // Align audio rendition with the "main" playlist on discontinuity change
583
+ // or program-date-time (PDT)
584
+ alignStream(mainDetails, newDetails, this);
585
+ sliding = newDetails.fragmentStart;
586
+ }
587
+ }
588
+ trackLevel.details = newDetails;
589
+ this.levelLastLoaded = trackLevel;
590
+
591
+ // compute start position if we are aligned with the main playlist
592
+ if (!this.startFragRequested) {
593
+ this.setStartPosition(mainDetails, sliding);
594
+ }
595
+
596
+ this.hls.trigger(Events.AUDIO_TRACK_UPDATED, {
597
+ details: newDetails,
598
+ id: trackId,
599
+ groupId: data.groupId,
600
+ });
601
+
602
+ // only switch back to IDLE state if we were waiting for track to start downloading a new fragment
603
+ if (
604
+ this.state === State.WAITING_TRACK &&
605
+ !this.waitForCdnTuneIn(newDetails)
606
+ ) {
607
+ this.state = State.IDLE;
608
+ }
609
+
610
+ // trigger handler right now
611
+ this.tick();
612
+ }
613
+
614
+ _handleFragmentLoadProgress(data: FragLoadedData) {
615
+ const frag = data.frag as MediaFragment;
616
+ const { part, payload } = data;
617
+ const { config, trackId, levels } = this;
618
+ if (!levels) {
619
+ this.warn(
620
+ `Audio tracks were reset while fragment load was in progress. Fragment ${frag.sn} of level ${frag.level} will not be buffered`,
621
+ );
622
+ return;
623
+ }
624
+
625
+ const track = levels[trackId] as Level;
626
+ if (!track) {
627
+ this.warn('Audio track is undefined on fragment load progress');
628
+ return;
629
+ }
630
+ const details = track.details as LevelDetails;
631
+ if (!details) {
632
+ this.warn('Audio track details undefined on fragment load progress');
633
+ this.removeUnbufferedFrags(frag.start);
634
+ return;
635
+ }
636
+ const audioCodec =
637
+ config.defaultAudioCodec || track.audioCodec || 'mp4a.40.2';
638
+
639
+ let transmuxer = this.transmuxer;
640
+ if (!transmuxer) {
641
+ transmuxer = this.transmuxer = new TransmuxerInterface(
642
+ this.hls,
643
+ PlaylistLevelType.AUDIO,
644
+ this._handleTransmuxComplete.bind(this),
645
+ this._handleTransmuxerFlush.bind(this),
646
+ );
647
+ }
648
+
649
+ // Check if we have video initPTS
650
+ // If not we need to wait for it
651
+ const initPTS = this.initPTS[frag.cc];
652
+ const initSegmentData = frag.initSegment?.data;
653
+ if (initPTS !== undefined) {
654
+ // this.log(`Transmuxing ${sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`);
655
+ // time Offset is accurate if level PTS is known, or if playlist is not sliding (not live)
656
+ const accurateTimeOffset = false; // details.PTSKnown || !details.live;
657
+ const partIndex = part ? part.index : -1;
658
+ const partial = partIndex !== -1;
659
+ const chunkMeta = new ChunkMetadata(
660
+ frag.level,
661
+ frag.sn,
662
+ frag.stats.chunkCount,
663
+ payload.byteLength,
664
+ partIndex,
665
+ partial,
666
+ );
667
+ transmuxer.push(
668
+ payload,
669
+ initSegmentData,
670
+ audioCodec,
671
+ '',
672
+ frag,
673
+ part,
674
+ details.totalduration,
675
+ accurateTimeOffset,
676
+ chunkMeta,
677
+ initPTS,
678
+ );
679
+ } else {
680
+ this.log(
681
+ `Unknown video PTS for cc ${frag.cc}, waiting for video PTS before demuxing audio frag ${frag.sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`,
682
+ );
683
+ const { cache } = (this.waitingData = this.waitingData || {
684
+ frag,
685
+ part,
686
+ cache: new ChunkCache(),
687
+ complete: false,
688
+ });
689
+ cache.push(new Uint8Array(payload));
690
+ if (this.state !== State.STOPPED) {
691
+ this.state = State.WAITING_INIT_PTS;
692
+ }
693
+ }
694
+ }
695
+
696
+ protected _handleFragmentLoadComplete(fragLoadedData: FragLoadedData) {
697
+ if (this.waitingData) {
698
+ this.waitingData.complete = true;
699
+ return;
700
+ }
701
+ super._handleFragmentLoadComplete(fragLoadedData);
702
+ }
703
+
704
+ private onBufferReset(/* event: Events.BUFFER_RESET */) {
705
+ // reset reference to sourcebuffers
706
+ this.mediaBuffer = null;
707
+ }
708
+
709
+ private onBufferCreated(
710
+ event: Events.BUFFER_CREATED,
711
+ data: BufferCreatedData,
712
+ ) {
713
+ this.bufferFlushed = this.flushing = false;
714
+ const audioTrack = data.tracks.audio;
715
+ if (audioTrack) {
716
+ this.mediaBuffer = audioTrack.buffer || null;
717
+ }
718
+ }
719
+
720
+ private onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
721
+ if (
722
+ !this.audioOnly &&
723
+ data.frag.type === PlaylistLevelType.MAIN &&
724
+ isMediaFragment(data.frag)
725
+ ) {
726
+ this.mainFragLoading = data;
727
+ if (this.state === State.IDLE) {
728
+ this.tick();
729
+ }
730
+ }
731
+ }
732
+
733
+ private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
734
+ const { frag, part } = data;
735
+ if (frag.type !== PlaylistLevelType.AUDIO) {
736
+ if (
737
+ !this.audioOnly &&
738
+ frag.type === PlaylistLevelType.MAIN &&
739
+ !frag.elementaryStreams.video &&
740
+ !frag.elementaryStreams.audiovideo
741
+ ) {
742
+ this.audioOnly = true;
743
+ this.mainFragLoading = null;
744
+ }
745
+ return;
746
+ }
747
+ if (this.fragContextChanged(frag)) {
748
+ // If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion
749
+ // Avoid setting state back to IDLE or concluding the audio switch; otherwise, the switched-to track will not buffer
750
+ this.warn(
751
+ `Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${
752
+ frag.level
753
+ } finished buffering, but was aborted. state: ${
754
+ this.state
755
+ }, audioSwitch: ${
756
+ this.switchingTrack ? this.switchingTrack.name : 'false'
757
+ }`,
758
+ );
759
+ return;
760
+ }
761
+ if (isMediaFragment(frag)) {
762
+ this.fragPrevious = frag;
763
+ const track = this.switchingTrack;
764
+ if (track) {
765
+ this.bufferedTrack = track;
766
+ }
767
+ }
768
+ this.fragBufferedComplete(frag, part);
769
+ if (this.media) {
770
+ this.tick();
771
+ }
772
+ }
773
+
774
+ protected getBufferOutput(): Bufferable | null {
775
+ return this.mediaBuffer ? this.mediaBuffer : this.media;
776
+ }
777
+
778
+ protected checkFragmentChanged() {
779
+ const previousFrag = this.fragPlaying;
780
+ const fragChanged = super.checkFragmentChanged();
781
+ if (!fragChanged) {
782
+ return false;
783
+ }
784
+
785
+ const fragPlaying = this.fragPlaying;
786
+ const fragPreviousLevel = previousFrag?.level;
787
+ if (!fragPlaying || fragPlaying.level !== fragPreviousLevel) {
788
+ if (previousFrag) {
789
+ this.cleanupBackBuffer();
790
+ }
791
+ if (this.switchingTrack) {
792
+ this.completeAudioSwitch(this.switchingTrack);
793
+ }
794
+ }
795
+ return true;
796
+ }
797
+
798
+ protected onError(event: Events.ERROR, data: ErrorData) {
799
+ if (data.fatal) {
800
+ this.state = State.ERROR;
801
+ return;
802
+ }
803
+ switch (data.details) {
804
+ case ErrorDetails.FRAG_GAP:
805
+ case ErrorDetails.FRAG_PARSING_ERROR:
806
+ case ErrorDetails.FRAG_DECRYPT_ERROR:
807
+ case ErrorDetails.FRAG_LOAD_ERROR:
808
+ case ErrorDetails.FRAG_LOAD_TIMEOUT:
809
+ case ErrorDetails.KEY_LOAD_ERROR:
810
+ case ErrorDetails.KEY_LOAD_TIMEOUT:
811
+ this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data);
812
+ break;
813
+ case ErrorDetails.AUDIO_TRACK_LOAD_ERROR:
814
+ case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT:
815
+ case ErrorDetails.LEVEL_PARSING_ERROR:
816
+ // in case of non fatal error while loading track, if not retrying to load track, switch back to IDLE
817
+ if (
818
+ !data.levelRetry &&
819
+ this.state === State.WAITING_TRACK &&
820
+ data.context?.type === PlaylistContextType.AUDIO_TRACK
821
+ ) {
822
+ this.state = State.IDLE;
823
+ }
824
+ break;
825
+ case ErrorDetails.BUFFER_ADD_CODEC_ERROR:
826
+ case ErrorDetails.BUFFER_APPEND_ERROR:
827
+ if (data.parent !== 'audio') {
828
+ return;
829
+ }
830
+ if (!this.reduceLengthAndFlushBuffer(data)) {
831
+ this.resetLoadingState();
832
+ }
833
+ break;
834
+ case ErrorDetails.BUFFER_FULL_ERROR:
835
+ if (data.parent !== 'audio') {
836
+ return;
837
+ }
838
+ if (this.reduceLengthAndFlushBuffer(data)) {
839
+ this.bufferedTrack = null;
840
+ super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio');
841
+ }
842
+ break;
843
+ case ErrorDetails.INTERNAL_EXCEPTION:
844
+ this.recoverWorkerError(data);
845
+ break;
846
+ default:
847
+ break;
848
+ }
849
+ }
850
+
851
+ private onBufferFlushing(
852
+ event: Events.BUFFER_FLUSHING,
853
+ { type }: BufferFlushingData,
854
+ ) {
855
+ if (type !== ElementaryStreamTypes.VIDEO) {
856
+ this.flushing = true;
857
+ }
858
+ }
859
+
860
+ private onBufferFlushed(
861
+ event: Events.BUFFER_FLUSHED,
862
+ { type }: BufferFlushedData,
863
+ ) {
864
+ if (type !== ElementaryStreamTypes.VIDEO) {
865
+ this.flushing = false;
866
+ this.bufferFlushed = true;
867
+ if (this.state === State.ENDED) {
868
+ this.state = State.IDLE;
869
+ }
870
+ const mediaBuffer = this.mediaBuffer || this.media;
871
+ if (mediaBuffer) {
872
+ this.afterBufferFlushed(mediaBuffer, type);
873
+ this.tick();
874
+ }
875
+ }
876
+ }
877
+
878
+ private _handleTransmuxComplete(transmuxResult: TransmuxerResult) {
879
+ const id = 'audio';
880
+ const { hls } = this;
881
+ const { remuxResult, chunkMeta } = transmuxResult;
882
+
883
+ const context = this.getCurrentContext(chunkMeta);
884
+ if (!context) {
885
+ this.resetWhenMissingContext(chunkMeta);
886
+ return;
887
+ }
888
+ const { frag, part, level } = context;
889
+ const { details } = level;
890
+ const { audio, text, id3, initSegment } = remuxResult;
891
+
892
+ // Check if the current fragment has been aborted. We check this by first seeing if we're still playing the current level.
893
+ // If we are, subsequently check if the currently loading fragment (fragCurrent) has changed.
894
+ if (this.fragContextChanged(frag) || !details) {
895
+ this.fragmentTracker.removeFragment(frag);
896
+ return;
897
+ }
898
+
899
+ this.state = State.PARSING;
900
+ if (audio && this.switchingTrack && !this.switchingTrack.flushImmediate) {
901
+ const { config } = this;
902
+ const bufferFlushDelay = config.nextAudioTrackBufferFlushForwardOffset;
903
+ const startOffset = Math.max(
904
+ this.getLoadPosition() + bufferFlushDelay,
905
+ frag.start,
906
+ );
907
+ super.flushMainBuffer(
908
+ startOffset,
909
+ Number.POSITIVE_INFINITY,
910
+ PlaylistLevelType.AUDIO,
911
+ );
912
+ }
913
+
914
+ if (initSegment?.tracks) {
915
+ const mapFragment = frag.initSegment || frag;
916
+ if (this.unhandledEncryptionError(initSegment, frag)) {
917
+ return;
918
+ }
919
+ this._bufferInitSegment(
920
+ level,
921
+ initSegment.tracks,
922
+ mapFragment,
923
+ chunkMeta,
924
+ );
925
+ hls.trigger(Events.FRAG_PARSING_INIT_SEGMENT, {
926
+ frag: mapFragment,
927
+ id,
928
+ tracks: initSegment.tracks,
929
+ });
930
+ // Only flush audio from old audio tracks when PTS is known on new audio track
931
+ }
932
+ if (audio) {
933
+ const { startPTS, endPTS, startDTS, endDTS } = audio;
934
+ if (part) {
935
+ part.elementaryStreams[ElementaryStreamTypes.AUDIO] = {
936
+ startPTS,
937
+ endPTS,
938
+ startDTS,
939
+ endDTS,
940
+ };
941
+ }
942
+ frag.setElementaryStreamInfo(
943
+ ElementaryStreamTypes.AUDIO,
944
+ startPTS,
945
+ endPTS,
946
+ startDTS,
947
+ endDTS,
948
+ );
949
+ this.bufferFragmentData(audio, frag, part, chunkMeta);
950
+ }
951
+
952
+ if (id3?.samples?.length) {
953
+ const emittedID3: FragParsingMetadataData = Object.assign(
954
+ {
955
+ id,
956
+ frag,
957
+ details,
958
+ },
959
+ id3,
960
+ );
961
+ hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3);
962
+ }
963
+ if (text) {
964
+ const emittedText: FragParsingUserdataData = Object.assign(
965
+ {
966
+ id,
967
+ frag,
968
+ details,
969
+ },
970
+ text,
971
+ );
972
+ hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText);
973
+ }
974
+ }
975
+
976
+ private _bufferInitSegment(
977
+ currentLevel: Level,
978
+ tracks: TrackSet,
979
+ frag: Fragment,
980
+ chunkMeta: ChunkMetadata,
981
+ ) {
982
+ if (this.state !== State.PARSING) {
983
+ return;
984
+ }
985
+ // delete any video track found on audio transmuxer
986
+ if (tracks.video) {
987
+ delete tracks.video;
988
+ }
989
+ if (tracks.audiovideo) {
990
+ delete tracks.audiovideo;
991
+ }
992
+
993
+ // include levelCodec in audio and video tracks
994
+ if (!tracks.audio) {
995
+ return;
996
+ }
997
+ const track = tracks.audio;
998
+
999
+ track.id = PlaylistLevelType.AUDIO;
1000
+
1001
+ const variantAudioCodecs = currentLevel.audioCodec;
1002
+ this.log(
1003
+ `Init audio buffer, container:${track.container}, codecs[level/parsed]=[${variantAudioCodecs}/${track.codec}]`,
1004
+ );
1005
+ // SourceBuffer will use track.levelCodec if defined
1006
+ if (variantAudioCodecs?.split(',').length === 1) {
1007
+ track.levelCodec = variantAudioCodecs;
1008
+ }
1009
+ this.hls.trigger(Events.BUFFER_CODECS, tracks as BufferCodecsData);
1010
+ const initSegment = track.initSegment;
1011
+ if (initSegment?.byteLength) {
1012
+ const segment: BufferAppendingData = {
1013
+ type: 'audio',
1014
+ frag,
1015
+ part: null,
1016
+ chunkMeta,
1017
+ parent: frag.type,
1018
+ data: initSegment,
1019
+ };
1020
+ this.hls.trigger(Events.BUFFER_APPENDING, segment);
1021
+ }
1022
+ // trigger handler right now
1023
+ this.tickImmediate();
1024
+ }
1025
+
1026
+ protected loadFragment(
1027
+ frag: Fragment,
1028
+ track: Level,
1029
+ targetBufferTime: number,
1030
+ ) {
1031
+ // only load if fragment is not loaded or if in audio switch
1032
+ const fragState = this.fragmentTracker.getState(frag);
1033
+
1034
+ // we force a frag loading in audio switch as fragment tracker might not have evicted previous frags in case of quick audio switch
1035
+ if (
1036
+ this.switchingTrack ||
1037
+ fragState === FragmentState.NOT_LOADED ||
1038
+ fragState === FragmentState.PARTIAL
1039
+ ) {
1040
+ if (!isMediaFragment(frag)) {
1041
+ this._loadInitSegment(frag, track);
1042
+ } else if (track.details?.live && !this.initPTS[frag.cc]) {
1043
+ this.log(
1044
+ `Waiting for video PTS in continuity counter ${frag.cc} of live stream before loading audio fragment ${frag.sn} of level ${this.trackId}`,
1045
+ );
1046
+ this.state = State.WAITING_INIT_PTS;
1047
+ const mainDetails = this.mainDetails;
1048
+ if (
1049
+ mainDetails &&
1050
+ mainDetails.fragmentStart !== track.details.fragmentStart
1051
+ ) {
1052
+ alignStream(mainDetails, track.details, this);
1053
+ }
1054
+ } else {
1055
+ super.loadFragment(frag, track, targetBufferTime);
1056
+ }
1057
+ } else {
1058
+ this.clearTrackerIfNeeded(frag);
1059
+ }
1060
+ }
1061
+
1062
+ private flushAudioIfNeeded(switchingTrack: AudioTrackSwitchingData) {
1063
+ if (this.media && this.bufferedTrack && switchingTrack.flushImmediate) {
1064
+ const { name, lang, assocLang, characteristics, audioCodec, channels } =
1065
+ this.bufferedTrack;
1066
+ if (
1067
+ !matchesOption(
1068
+ { name, lang, assocLang, characteristics, audioCodec, channels },
1069
+ switchingTrack,
1070
+ audioMatchPredicate,
1071
+ )
1072
+ ) {
1073
+ if (useAlternateAudio(switchingTrack.url, this.hls)) {
1074
+ this.log('Switching audio track : flushing all audio');
1075
+ super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio');
1076
+ this.bufferedTrack = null;
1077
+ } else {
1078
+ // Main is being buffered. Set bufferedTrack so that it is flushed when switching back to alt-audio
1079
+ this.bufferedTrack = switchingTrack;
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+
1085
+ private completeAudioSwitch(switchingTrack: AudioTrackSwitchingData) {
1086
+ const { hls } = this;
1087
+ this.bufferedTrack = switchingTrack;
1088
+ this.switchingTrack = null;
1089
+ hls.trigger(Events.AUDIO_TRACK_SWITCHED, { ...switchingTrack });
1090
+ }
1091
+
1092
+ /**
1093
+ * Index of next audio track loaded as scheduled by audio stream controller.
1094
+ */
1095
+ get nextAudioTrack(): number {
1096
+ return this.nextTrackId;
1097
+ }
1098
+ }
1099
+ export default AudioStreamController;