@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,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;
|